From a7c8f36b9f6803112ca5d0cc7f2570b67068c064 Mon Sep 17 00:00:00 2001 From: Monica Peters Date: Mon, 27 Apr 2026 21:06:54 -0400 Subject: [PATCH 01/82] add ARCHITECTURE.md, USERS.md and AUDIT.md for Week 1 PRD work. --- ARCHITECTURE.md | 292 ++++++++++++++++++++++++++++++++++++++++++++++++ AUDIT.md | 185 ++++++++++++++++++++++++++++++ USERS.md | 107 ++++++++++++++++++ 3 files changed, 584 insertions(+) create mode 100644 ARCHITECTURE.md create mode 100644 AUDIT.md create mode 100644 USERS.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000000..529f0ddd9707 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,292 @@ +# AgentForge Clinical Co-Pilot — Technical architecture + +**Upstream:** [openemr/openemr](https://github.com/openemr/openemr/tree/master) +**License context:** OpenEMR is GPL-3.0-or-later; this fork’s additive documentation does not change upstream licensing. + +--- + +## One-page summary (~500 words) + +OpenEMR is a mature monolithic electronic health record with multiple architectural eras: a growing PSR-4 `OpenEMR\` tree under `src/`, legacy procedural code under `library/` and `interface/`, a PSR-11 bootstrap (`bootstrap.php`) used by the modern front controller (`public/index.php`), and Laminas MVC modules for selected subsystems. The Clinical Co-Pilot is designed to **sit inside that reality** rather than replace it: the agent’s **trust boundary** is the **same authenticated PHP request** as the rest of the clinician UI, using **in-process** calls into OpenEMR’s **authorization and data-access patterns**, not a parallel “super-user” FHIR client for this sprint. + +The user is a **high-volume primary care physician** (see [USERS.md](USERS.md)). The highest-value window is **between exam rooms**, when the physician must reconstruct context in under a minute. That imposes **strict latency goals** and a **speed-versus-completeness** tradeoff: the system should return a **thin, verifiable briefing** quickly, support **follow-up questions**, and **label uncertainty** when the chart is incomplete. + +**Large language model:** The sprint uses the **OpenAI HTTP API** against **synthetic demo chart content only** (per program instructions). Production-grade deployment would require **Business Associate Agreements**, **minimum necessary** payloads, **subprocessor governance**, and **verified** data retention and training exclusions—documented as a forward path, not assumed solved by the class environment. + +**Tooling strategy:** Tools are **PHP functions or thin controller handlers** that gather **structured patient data** by reusing existing OpenEMR services (for example `OpenEMR\Services\PatientService` and other domain services under `src/Services/`) **only after** the request is operating under a **valid staff session** and **active patient context**, with the same **access checks** the UI would rely on. Tools return **JSON** suitable for model consumption (field-limited, no full chart dumps by default). + +**Verification:** The model emits a **structured response schema** pairing **factual clinical statements** with **citation handles** referencing tool output (record IDs, table row references, or opaque server-side keys that resolve only server-side). A **PHP verification gate** strips or downgrades any **uncited factual claim** before the UI renders it. **Domain constraints** (e.g. hard limits on dosing suggestions, or prohibition on definitive new diagnoses) are enforced in **PHP rules**, not delegated to the model. + +**Observability:** The architecture compares **four** patterns: (1) **Langfuse** or similar SaaS with **strict redaction** and no raw PHI; (2) **self-hosted** trace store inside the clinic VPC; (3) a **dedicated database table** storing redacted step metadata; (4) **application JSON logs** only. For the sprint, the **default recommendation** is **(4) plus optional (3)** for demo-friendly audit trails, moving to **(2)** before any real PHI. + +**Failure modes:** Tool timeouts, partial records, and model **malformation** must yield **degraded but honest** responses—never silent wrong facts. The UI shows **which tools ran**, what **failed**, and what **could not be verified**. + +**Evolution:** If the product graduates beyond synthetic demos, the documented **production** path is **OAuth2-scoped FHIR or Standard API** tools with explicit client scoping—without changing the core principle that **authorization is never delegated to the LLM**. + +Agent tools execute in-process under the authenticated OpenEMR session, delegating to the same authorization and data-access layers as the clinical UI; we are not exposing a separate broad FHIR client for this sprint, because of this project’s scope, and we document the production path to OAuth2-scoped tools if the agent were promoted beyond synthetic data. + +--- + +## Table of contents + +1. [Repository scope (Week 1 / PRD)](#repository-scope-week-1--prd) +2. [Goals and non-goals](#goals-and-non-goals) +3. [Alignment with USERS.md](#alignment-with-usersmd) +4. [High-level system diagram](#high-level-system-diagram) +5. [Trust boundaries](#trust-boundaries) +6. [Request path and OpenEMR integration](#request-path-and-openemr-integration) +7. [Tool layer](#tool-layer) +8. [LLM integration (OpenAI)](#llm-integration-openai) +9. [Verification pipeline](#verification-pipeline) +10. [Observability options](#observability-options) +11. [Evaluation hooks](#evaluation-hooks) +12. [AI cost analysis (planning)](#ai-cost-analysis-planning) +13. [Failure modes and degradation](#failure-modes-and-degradation) +14. [Deployment (TBD checklist)](#deployment-tbd-checklist) +15. [References within this repository](#references-within-this-repository) + +--- + +## Repository scope (Week 1 / PRD) + +**Single source of truth in this fork:** Week 1 AgentForge planning, hard-gate documentation, deployment URL for submissions, and summaries of eval intent and AI cost **live in this repository**—primarily in [AUDIT.md](AUDIT.md), [USERS.md](USERS.md), and this file—alongside [`Documentation/PRD_Week1_AgentForge.pdf`](Documentation/PRD_Week1_AgentForge.pdf). Avoid parallel specs in external tools that can drift from what graders see in GitHub. + +**Capability boundary:** [USERS.md](USERS.md) defines the **only** user problems the agent may address; every tool, prompt, and UI behavior in later implementation must **trace to a use case** there. The audit ([AUDIT.md](AUDIT.md)) is the **input** to this architecture plan (PRD Stage 5); implementation should not outrun audited risks. + +**PRD checkpoint → document map** + +| PRD stage / gate | Where it is satisfied in-repo | +|------------------|-------------------------------| +| Stage 3 — Audit | [AUDIT.md](AUDIT.md) (five audit areas + ~500 word summary) | +| Stage 4 — Users & use cases | [USERS.md](USERS.md) (narrow user + use cases + why conversational) | +| Stage 5 — Agent integration plan | This file (~500 word summary + technical sections) | +| Observability minimum questions | [Observability options](#observability-options) | +| Evaluation / test suite | [Evaluation hooks](#evaluation-hooks); detailed fixtures may live under `tests/` when code exists | +| Submitted deployed URL | [Deployment (TBD checklist)](#deployment-tbd-checklist) — canonical URL field | + +**Course deliverables vs. these three files:** The PRD also names a fork **README** (setup guide), an **eval dataset with results**, and **AI cost analysis**. If instructors require those as **separate committed files**, add or extend them outside this trio and keep one-line pointers here. If not, treat the sections [Local setup and deployment pointers](#local-setup-and-deployment-pointers), [Evaluation hooks](#evaluation-hooks), and [AI cost analysis (planning)](#ai-cost-analysis-planning) as the in-repo record. + +--- + +## Goals and non-goals + +**Goals** + +- **Sub-minute** orientation for the PCP use cases in [USERS.md](USERS.md). +- **Citation-backed** clinical statements for chart-derived facts. +- **Server-side-only** orchestration (no PHI-bearing tool calls from untrusted browser code). +- **Additive** implementation: prefer new module routes and services over forking core legacy files. + +**Non-goals (sprint)** + +- Autonomous clinical decision-making or autonomous ordering. +- Full FHIR tool chain **inside** the sprint (see summary sentence above). +- Training or fine-tuning a model on customer data. + +--- + +## Alignment with USERS.md + +| US case | Architectural components | +|---------|----------------------------| +| UC1 Visit framing | Encounters + problems tools; summarization prompt; verification | +| UC2 Deltas | Labs/meds tools with “since last visit” windowing; uncertainty labels | +| UC3 Pre-room checks | Rule engine + cited suggestions; explicit “verify with patient” copy | + +--- + +## High-level system diagram + +```mermaid +flowchart TB + subgraph browser [Browser] + UI[CoPilot_UI] + end + subgraph openemr [OpenEMR_PHP_Session] + EP[Agent_Endpoint] + ACL[Session_ACL_PatientContext] + TL[Tool_Layer] + SV[Domain_Services_src_Services] + VG[Verification_Gate] + end + subgraph external [External] + OAI[OpenAI_API] + end + UI -->|HTTPS_same_origin| EP + EP --> ACL + EP --> TL + TL --> SV + EP --> OAI + OAI -->|structured_model_output| VG + TL -->|tool_JSON_citations| VG + VG -->|safe_payload| UI +``` + +--- + +## Trust boundaries + +| Boundary | Responsibility | +|----------|------------------| +| Browser ↔ OpenEMR | Standard OpenEMR session cookies / CSRF patterns for new endpoints. | +| OpenEMR ↔ OpenAI | **Minimum necessary** JSON; TLS; API key in server config only; **no PHI in client-accessible logs**. | +| Model ↔ User | **No** unverified factual text; verification gate is mandatory. | + +--- + +## Request path and OpenEMR integration + +Modern requests may enter through [`public/index.php`](public/index.php), which loads the PSR-11 container from [`bootstrap.php`](bootstrap.php) and delegates routing via `OpenEMR\BC\FallbackRouter` to legacy scripts. Agent endpoints should follow **the same authentication bootstrap** as other privileged PHP entrypoints (exact file(s) depend on implementation choice: new route under `interface/` or custom module pattern). + +**Principle:** The agent never receives a **wider** data scope than the logged-in user for the **currently selected patient**. + +--- + +## Tool layer + +**Design** + +- Each tool: **name**, **input schema** (patient id, date windows, optional question), **output JSON**, **max runtime**, **idempotent** where possible. +- Tools call **`OpenEMR\Services\*`** or other approved internal APIs—not raw SQL from the model. + +**Initial tool candidates (illustrative)** + +| Tool | Purpose | Typical service anchor | +|------|---------|-------------------------| +| `patient_core` | Demographics, PCCP, identifiers | `OpenEMR\Services\PatientService` | +| `active_meds` | Medication list snapshot | Domain medication services | +| `recent_labs` | Windowed labs | Lab result services | +| `recent_encounters` | Visit list / reasons | Encounter-related services | + +*(Exact method names should be pinned when code is written; PatientService is the canonical patient row access pattern in `src/Services/PatientService.php`.)* + +--- + +## LLM integration (OpenAI) + +- **Transport:** HTTPS to OpenAI’s API from PHP (e.g. Guzzle — already a project dependency per `composer.json`). +- **Configuration:** API key from environment or secured config **outside** webroot; never embedded in frontend. +- **Prompting:** System prompt encodes **citation requirements**, **refusal rules**, and **scope** (active patient only). +- **BAA / enterprise:** Document in [AUDIT.md](AUDIT.md) compliance section; use **Azure OpenAI** or **OpenAI Enterprise** if a real hospital required a single named BAA—sprint may use developer API **only with synthetic data**. + +--- + +## Verification pipeline + +1. **Plan:** Model may emit a hidden chain-of-thought **only if** your vendor/policy allows; otherwise use **structured tool plans** without chain-of-thought storage. +2. **Tool execution:** Server runs tools; results stored **in memory** for the request (or short-lived server-side cache keyed by session). +3. **Model answer:** Must conform to JSON schema: `{ "statements": [ { "text", "citations": ["..."] } ], "uncertainties": [...] }`. +4. **PHP gate:** Drop statements whose citations do not resolve to tool JSON; convert borderline cases to **uncertainty** strings. +5. **Rule engine:** Secondary pass for **forbidden content** (e.g. definitive new cancer diagnosis strings) regardless of citations. + +--- + +## Observability options + +| Option | Pros | Cons | PHI risk | +|--------|------|------|----------| +| **A. Langfuse (cloud)** | Rich traces, token/cost dashboards | Third-party subprocessors | **High** if raw prompts logged | +| **B. Self-hosted trace DB** | Control, VPC-only | Ops burden | **Medium**—must redact | +| **C. OpenEMR DB audit table** | Stays in app boundary | Schema + migration work | **Low** if redacted columns only | +| **D. PHP JSON logs** | Fastest sprint path | Weaker UI | **Low** with redaction | + +**Sprint recommendation:** **D**, optionally **C** for structured step timing; migrate toward **B** before production PHI. + +**Minimum questions observability must answer (PRD):** + +- What did the agent do, **in order**? +- How long did each step take? +- Which tools failed and why? +- Token usage and **estimated cost** per request? + +**PRD bar (“real, wired in, used”):** Observability is not satisfied by installing a library alone. The chosen approach must emit structured events or logs on **actual** agent requests, be **consulted** when debugging (e.g. step timing, tool errors), and inform cost/token discipline—not a dormant dependency. + +--- + +## Evaluation hooks + +**Intent (PRD):** The suite must be **defensible**—not only happy paths. It should surface failure modes that matter in clinical settings: missing chart fields, ambiguous user phrasing, tool timeouts, malformed model JSON, and **unauthorized** data access attempts. + +| Category | What “pass” means (examples) | +|----------|----------------------------| +| **Schema / verification** | Output JSON validates; uncited factual statements are stripped or downgraded. | +| **Tool + gate integration** | Given golden tool JSON fixtures, rendered user text matches expected safe payload. | +| **Authorization** | User/session A cannot obtain patient B’s tool payloads (negative tests). | +| **Resilience** | Simulated OpenAI errors or empty tool results yield explicit degradation, not silent invention. | + +**Mechanisms:** Unit tests for JSON schema and verification gate; integration tests with **mocked** OpenAI; synthetic fixtures only (**no real PHI**). + +**Results log (update when runs exist):** Record date, command (e.g. `phpunit` target), pass/fail counts, and notable regressions in a bullet list here or in CI output linked from the fork. Until tests land, this subsection remains the **planned** eval contract. + +--- + +## AI cost analysis (planning) + +**Development spend:** Track actual API spend during integration (model choice, average tools per request, tokens in/out). Update this subsection with rough monthly dev totals when available. + +**Projected load (illustrative axes—replace with measured averages):** Assume *N* concurrent clinicians, *R* requests per clinician per hour, and *T* total input+output tokens per request (including tool JSON). Cost scales with *N × R × T ×* price-per-token; tool-heavy flows increase *T* faster than chat-only flows. + +| Scale (active clinical users) | Architectural implication | +|------------------------------|----------------------------| +| **~100** | Single-region OpenEMR + rate limits; shared API key pool with per-tenant budgets if multi-site. | +| **~1K** | Queue or throttle agent requests; cache **non-PHI** or redacted short-lived briefings where policy allows; consider reserved throughput / enterprise API. | +| **~10K** | Regional deployment, aggressive **minimum-necessary** prompts, model tiering (smaller model for routing), batch where safe—not linear “token × users” without redesign. | +| **~100K** | Dedicated inference contracts, possible **on-VPC** or Azure OpenAI–class deployments; observability and cost allocation per site/department. | + +This is **not** “cost-per-token × users” alone; bounded tools, verification passes, and caching policy dominate at scale. + +--- + +## Failure modes and degradation + +| Failure | User-visible behavior | +|---------|-------------------------| +| Tool timeout | “Labs unavailable—showing problems only.” | +| Empty chart section | Explicit **missing data** label. | +| Model JSON invalid | One automatic **repair** attempt; else safe error. | +| OpenAI outage | Cached last-good briefing **if policy allows**; else clear outage message. | + +--- + +## Deployment (TBD checklist) + +**Canonical deployed application URL (PRD submissions):** +`TBD` — replace at each checkpoint (MVP, Early Submission, Final) with the **publicly reachable** URL of this fork’s deployment; keep in sync with what you submit to the course. + +**Hosting not yet selected.** Before going live even with synthetic data, complete: + +- [ ] TLS certificate and HTTPS-only cookies +- [ ] Secrets management (API keys) +- [ ] Admin password rotation / demo banner +- [ ] Log rotation and **redaction** rules +- [ ] Backup policy for DB (if storing traces) +- [ ] Rollback: prior container image or release tag + +### Local setup and deployment pointers + +- **Local OpenEMR:** Follow upstream guidance (e.g. Docker or stack docs from [openemr/openemr](https://github.com/openemr/openemr)); fork-specific env vars or compose overrides should be **documented here in bullet form** as you stabilize them so Week 1 “Run it locally” stays traceable in-repo. +- **Public deploy:** Same stack family as intended for the final agent reduces surprise; record provider and **runtime** (PHP version, extensions) briefly here once chosen. + +--- + +## References within this repository + +| Topic | Location | +|--------|----------| +| Front controller + DI bootstrap | [`public/index.php`](public/index.php), [`bootstrap.php`](bootstrap.php) | +| Legacy routing bridge | [`src/BC/FallbackRouter.php`](src/BC/FallbackRouter.php) | +| Modern services | [`src/Services/`](src/Services/) (e.g. [`src/Services/PatientService.php`](src/Services/PatientService.php)) | +| API / OAuth / FHIR (future path) | [`Documentation/api/`](Documentation/api/) | +| Contributor / quality bar | [`CLAUDE.md`](CLAUDE.md) | +| Security reporting | [`.github/SECURITY.md`](.github/SECURITY.md) | +| PRD | [`Documentation/PRD_Week1_AgentForge.pdf`](Documentation/PRD_Week1_AgentForge.pdf) | + +--- + +## Document control + +| Field | Value | +|--------|--------| +| **Project** | AgentForge — Clinical Co-Pilot | +| **Companion documents** | [USERS.md](USERS.md), [AUDIT.md](AUDIT.md) | +| **PRD (Week 1)** | [`Documentation/PRD_Week1_AgentForge.pdf`](Documentation/PRD_Week1_AgentForge.pdf) — submission table lists `./USER.md`; this fork uses **`./USERS.md`** at repo root (align with graders if needed). | diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 000000000000..5072eac5f6a3 --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,185 @@ +# AgentForge Clinical Co-Pilot — System and fork audit + +**Upstream:** [openemr/openemr](https://github.com/openemr/openemr/tree/master) +**Audit scope:** Baseline OpenEMR architecture and security posture **as observed in repository documentation and representative entrypoint code**, plus **HIPAA-relevant implications** for integrating a Clinical Co-Pilot. This audit supports [USERS.md](USERS.md) and is the **primary input** to the forward-looking integration plan in [ARCHITECTURE.md](ARCHITECTURE.md) (PRD Stage 5 synthesizes audit + users into architecture). **No production PHI** is used in class work—findings assume **synthetic demo data** unless otherwise noted. + +--- + +## One-page summary (~500 words) + +OpenEMR is a large, long-lived **PHP monolith** combining modern and legacy layers: PSR-4 code under `src/` (namespaced `OpenEMR\`), procedural and template-driven UI under `interface/` and `library/`, optional **Laminas MVC** modules, and a **progressive front controller** in `public/index.php` that loads a **PSR-11 container** from `bootstrap.php` then includes legacy scripts via `OpenEMR\BC\FallbackRouter`. That hybrid model is a **strength** for incremental features but a **risk** for uniform security policy: not every path inherits the same hardening or logging discipline, and new features must **explicitly** reapply authorization, output encoding, and audit expectations. + +For **HIPAA-aligned design**, the critical dimensions are **where PHI lives** (database, session, logs, backups), **how it moves** (TLS to clients and to subprocessors such as LLM vendors), **who can access it** (role-based access, patient context, API scopes), and **what is logged** (application logs, web server logs, APM traces). OpenEMR provides **serious building blocks**: documented **OAuth2 / FHIR / SMART** flows under `Documentation/api/`, security disclosure processes in `.github/SECURITY.md`, extensive automated testing and static analysis (see `CLAUDE.md` and CI badges in `README.md`), and domain services such as `OpenEMR\Services\PatientService` that centralize data access patterns for modern code. **Gaps** remain: legacy patterns documented in `CLAUDE.md` (globals, mixed-era templates) increase **review burden** and the chance of **inconsistent audit trails** for novel features if implementers bypass established services. + +**Performance** for an AI co-pilot is dominated by **LLM latency** and **tool fan-out** (database reads, serialization). OpenEMR’s traditional pages can be **heavy** on first load; an agent that issues **wide** queries will amplify bottlenecks. The audit recommends **bounded tools**, **parallelism only where safe**, and **progressive disclosure** of detail to meet **sub-minute** PCP expectations from [USERS.md](USERS.md). + +**Data quality** is uneven in any real EHR: duplicate problems, stale meds, scanned PDFs versus structured entries, and unsigned notes. **Agent failure modes** mirror these gaps; verification must treat **missing** and **ambiguous** as first-class outcomes. + +**Compliance:** Class deployment uses **synthetic chart text** and may call **OpenAI’s API**, but **production PHI** would require **Business Associate Agreements**, **minimum necessary** payloads, **training / retention** guarantees, subprocessors review, and likely **Azure OpenAI** or equivalent enterprise contracts. **Public observability SaaS** is **high risk** for PHI unless **redacted** or **self-hosted**; default recommendation is **server-side redacted logs** (see [ARCHITECTURE.md](ARCHITECTURE.md)). + +**Bottom line:** OpenEMR is a **credible foundation** for a **session-bound, in-process** Clinical Co-Pilot that **reuses existing authorization layers** and limits LLM exposure to **necessary structured excerpts**. The highest-impact audit outcome is **not** “avoid OpenEMR,” but **control integration surface area**: strict tool contracts, mandatory verification, **no PHI in third-party traces**, and a documented **upgrade path** to OAuth2-scoped APIs before real clinical deployment. + +--- + +## Table of contents + +1. [Security audit](#security-audit) +2. [Performance audit](#performance-audit) +3. [Architecture audit](#architecture-audit) +4. [Data quality audit](#data-quality-audit) +5. [Compliance and regulatory audit](#compliance-and-regulatory-audit) +6. [Prioritized findings](#prioritized-findings) +7. [References](#references) + +--- + +## Security audit + +### Authentication and session model + +- Staff workflows rely on traditional **web session** authentication established through `interface/` login flows (not re-audited line-by-line here). +- **API access** can follow **OAuth2** as documented in `Documentation/api/`—relevant for **future** tool paths, not required for the sprint’s **in-process** approach ([ARCHITECTURE.md](ARCHITECTURE.md)). + +### Authorization risks + +| Risk | Description | Mitigation in Co-Pilot | +|------|-------------|-------------------------| +| **IDOR / wrong patient** | Agent UI might pass patient identifiers incorrectly. | Bind tools to **server-side active patient** from session; ignore client-supplied IDs except as signed opaque tokens if needed. | +| **Over-privileged tools** | Tools that read entire chart. | **Field-limited** tool outputs; separate tools per domain. | +| **Prompt injection** | Chart text contains adversarial instructions. | Treat chart text as **data**, not instructions; system prompt hardening; tool allowlists. | + +### Data exposure vectors + +- **Web server access logs** (URLs may leak identifiers if query strings are misused). +- **PHP error logs** (stack traces can include SQL or paths). +- **LLM vendor logs** (policy-dependent; assume sensitive). +- **Observability third parties** (default: **do not send** raw prompts/responses). + +### PHI handling gaps (inherent to integration, not solely OpenEMR) + +- Any new endpoint must **mirror** existing ACL checks; absence is a **finding** on the fork’s implementation until proven by tests. + +--- + +## Performance audit + +| Area | Observation | Impact on agent | +|------|-------------|-----------------| +| Monolith + DB | Chart reads can be **expensive** when unbounded. | Tool queries must be **windowed** and indexed-friendly. | +| Cold start | First request may load many includes. | Prefer **small** agent endpoints; lazy-load heavy deps. | +| LLM RTT | Often **dominant** vs PHP. | Minimize round-trips; combine tools where safe. | + +**Suggested budgets (planning, not measured here):** + +- **Tools total:** target \< **1–2 s** p95 on demo hardware for initial briefing payload. +- **Model:** depends on OpenAI model choice; stream tokens to UI if used. + +--- + +## Architecture audit + +| Layer | Location (examples) | Notes | +|-------|---------------------|--------| +| Front controller | `public/index.php`, `bootstrap.php` | DI container without DB in bootstrap (by design). | +| Routing bridge | `src/BC/FallbackRouter.php` | Resolves to legacy script includes; large legacy surface. | +| Modern domain logic | `src/Services/` | Preferred integration point for tools. | +| UI / legacy | `interface/`, `library/` | High variety; integration should be **additive**. | +| APIs / interop | `Documentation/api/` | Production-grade external integration path. | + +```mermaid +flowchart LR + subgraph modern [Modern_entry] + FC[public_index_php] + CT[bootstrap_container] + FR[FallbackRouter] + end + subgraph legacy [Legacy_core] + IF[interface_scripts] + LB[library] + end + subgraph domain [Domain] + SV[src_Services] + end + FC --> CT --> FR --> IF + IF --> LB + IF --> SV +``` + +--- + +## Data quality audit + +| Issue | Agent impact | Mitigation | +|-------|----------------|-------------| +| Missing visit reason | Cannot infer “why today” | Explicit **missing** state | +| Duplicate problems | Confusing summary | De-duplicate in tool layer | +| Med list vs actual use | Wrong med story | Label as “recorded meds” | +| PDF-only labs | Model cannot parse | Omit or OCR **out of sprint** | + +--- + +## Compliance and regulatory audit + +**Disclaimer:** This section is **engineering guidance**, not legal advice. + +### HIPAA themes relevant to the Co-Pilot + +| Safeguard category | Question for the fork | Status | +|---------------------|-------------------------|--------| +| **Access (§164.312(a)(1))** | Are agent endpoints user- and role-bound? | **Design required** | +| **Audit (§164.312(b))** | Are agent reads/writes logged appropriately? | **Design required** | +| **Integrity (§164.312(c)(1))** | Can prompts/responses be tampered post hoc? | Mitigate with **append-only** internal audit if needed | +| **Transmission (§164.312(e)(1))** | TLS to browser and to OpenAI | **Required** | + +### LLM subprocessors (OpenAI) + +- Review **BAA availability** (often via **Azure OpenAI** or enterprise programs for covered entities). +- Contract terms: **no training** on customer API data (verify current OpenAI / Microsoft documentation at deployment time). +- **Minimum necessary:** send **structured excerpts**, not full charts. + +### Breach notification and retention + +- If logs contain **PHI**, they become **high-sensitivity assets** with retention limits. +- Sprint approach: **synthetic PHI only**; logs **redacted**. + +### BAA assumption (per PRD) + +Gauntlet Week 1 PRD: act **as if** a **Business Associate Agreement** (or equivalent) covers LLM vendors and that customer API data is **not used for model training**—while still using **demo / synthetic data only** in this codebase. Before any **real PHI**, replace that assumption with **written** vendor posture (BAA, retention, subprocessors, regions). + +--- + +## Prioritized findings + +| ID | Severity | Finding | Recommendation | +|----|----------|---------|----------------| +| F1 | High | LLM + observability can **exfiltrate PHI** via logs | Redact; prefer self-hosted or DB-stored redacted traces | +| F2 | High | New agent endpoints could **skip ACL** if rushed | Central middleware enforcing patient + permission | +| F3 | Medium | Legacy surface increases **audit inconsistency** | Reuse `src/Services` patterns; add tests | +| F4 | Medium | Data quality causes **hallucination-like** failures | Verification + “missing data” UX | +| F5 | Low | Performance surprises under multi-tool calls | Bounded concurrency + profiling | + +--- + +## References + +| Artifact | Path | +|----------|------| +| PRD | `Documentation/PRD_Week1_AgentForge.pdf` | +| Architecture decisions | [ARCHITECTURE.md](ARCHITECTURE.md) | +| Personas / use cases | [USERS.md](USERS.md) | +| Front controller | `public/index.php` | +| Bootstrap / DI | `bootstrap.php` | +| Fallback router | `src/BC/FallbackRouter.php` | +| Patient service example | `src/Services/PatientService.php` | +| API documentation | `Documentation/api/README.md` | +| Development standards | `CLAUDE.md` | +| Security disclosure | `.github/SECURITY.md` | + +--- + +## Document control + +| Field | Value | +|--------|--------| +| **Project** | AgentForge — Clinical Co-Pilot | +| **Companion documents** | [USERS.md](USERS.md), [ARCHITECTURE.md](ARCHITECTURE.md) (architecture **consumes** this audit) | diff --git a/USERS.md b/USERS.md new file mode 100644 index 000000000000..de7017975b44 --- /dev/null +++ b/USERS.md @@ -0,0 +1,107 @@ +# AgentForge Clinical Co-Pilot — User definition and use cases + +This document is the **source of truth** for *who* the Clinical Co-Pilot serves and *which problems* it solves. Every agent capability in implementation and in [ARCHITECTURE.md](ARCHITECTURE.md) must trace to a use case listed here. + +**Agent surface area (PRD):** Features that do not map to a use case below—including extra tools, multi-turn flows, or “nice to have” chat—are **out of scope** until this document is updated deliberately. The bar from the Week 1 PRD is whether a **narrow, real** user would **choose** this agent shape over a dashboard or better chart navigation. + +--- + +## Target user + +**Role:** A **deliberately narrow** persona for Week 1: board-certified **primary care physician** (family medicine or general internal medicine)—not “all clinicians” or a generic “physician needs information” thesis. + +**Setting:** Community **outpatient clinic** with a **high-volume schedule** (approximately fifteen to twenty-five face-to-face visits per day), mixed acute and chronic care, plus inbox and results tasks between visits. + +**Technical context:** The physician already uses **OpenEMR** for scheduling, charting, e-prescribing, and lab/imaging review. They move between exam rooms with **roughly one minute or less** between patients to re-orient on the next chart. + +**Goals:** Minimize cognitive load, avoid missing important changes since the last visit, and enter the room with an accurate mental model without reading the entire chart. + +**Constraints:** They will **not** adopt a tool that adds unreliable “facts,” hides uncertainty, or slows the workflow beyond a few seconds for the initial briefing. + +--- + +## Day-in-the-life workflow (where the agent appears) + +1. **Morning:** Review schedule; identify complex visits; skim open tasks. +2. **Between rooms (critical window):** Open the **active patient** in OpenEMR; need a **fast briefing** (who, why today, what changed, what to verify in the room). +3. **In the room:** Confirm with the patient; document; reconcile meds and problems. +4. **After block:** Close loops (orders, referrals, messaging). + +The agent is positioned primarily in **step 2**—the **inter-visit gap**—with optional short follow-up questions after the physician has seen a specific data item in the chart. + +--- + +## User interface entry point (sprint scope) + +**Planned surface:** A **dedicated “Clinical Co-Pilot” panel** within the existing OpenEMR **patient-centric** workflow—for example a **tab or slide-out** on the **patient summary / demographics** context or the **encounter** screen—implemented as **new additive UI** that posts to **server-side PHP** endpoints only. No standalone SPA that holds PHI outside the authenticated OpenEMR session. + +*(Exact screen name may match your fork’s first implementation; the requirement is: same login session and patient selection as the rest of the chart.)* + +--- + +## Use cases + +Each use case below includes **why a conversational agent** is appropriate (per AgentForge PRD), not merely “because AI is available.” If a capability cannot cite one of these use cases and justify **multi-turn** or **tool** behavior against that need, it should not ship in the sprint. + +### Use case 1 — “Who am I seeing next, and why today?” + +**Moment:** Between rooms, **thirty to ninety seconds** before entering the next visit. + +**Need:** A **coherent narrative**: chief complaint for today (if documented), recent relevant diagnoses, and the **purpose of this visit** without reading every historical note. + +**Why conversational (not only a dashboard):** The physician’s question is **naturally phrased** (“What’s the one-liner on this visit?”) and may **branch** (“What did we do last time for the same complaint?”). A static widget cannot answer **follow-ups** without pre-building every drill path. Multi-turn dialogue matches **exploratory recall** under time pressure. + +**Agent boundaries:** Answers must be **grounded in chart data** with **explicit citations** to sources (see [ARCHITECTURE.md](ARCHITECTURE.md) verification). If today’s visit reason is missing, the agent **states the gap** instead of inventing a reason. + +--- + +### Use case 2 — “What changed since I last saw them?” + +**Moment:** Same inter-visit window, or **first click** when opening a patient with a long interval since last visit. + +**Need:** **Delta-oriented** summary: new labs or imaging since last encounter, med list changes, new outside records if present, new allergies or problems—**prioritized** by clinical relevance for primary care (not a raw feed). + +**Why conversational:** “What changed?” is **underspecified**; the follow-up depends on what the chart contains (“Any new A1c?” “Did cardiology change their beta blocker?”). Conversation supports **progressive refinement** faster than clicking through five modules. + +**Agent boundaries:** Only report deltas **supported by structured or cited narrative data**; distinguish **“not in chart”** from **“unchanged.”** + +--- + +### Use case 3 — “What should I double-check before I walk in?” + +**Moment:** Immediately before rooming, for **high-risk** or **complex** patients (polypharmacy, recent ED visit, abnormal trending labs). + +**Need:** A **short checklist** of items to verbally verify with the patient or to re-check in the record (e.g. adherence-sensitive meds, pending results not reviewed, care gaps). + +**Why conversational:** The physician may ask **risk-tailored** questions (“Anything pregnancy-related?” “Any red-flag symptoms documented?”) that vary by patient; a fixed checklist template **over- or under-shoots**. The agent proposes **contextual prompts** the user can accept or dismiss in one or two turns. + +**Agent boundaries:** Items are **suggestions tied to citations**, not autonomous clinical decisions. Any **drug interaction or dosing “flag”** must follow the project’s **domain-constraint** rules in architecture (rules engine / hard rejects), not model improvisation. + +--- + +## Traceability matrix (for implementation planning) + +| Use case | Minimum data domains | Primary risk if wrong | Mitigation (architecture) | +|----------|----------------------|------------------------|---------------------------| +| UC1 Visit framing | Encounters, problem list, recent notes | Hallucinated visit reason | Structured citations + PHP verification gate | +| UC2 Deltas | Labs, meds, allergies, key vitals | False “no change” / missed new result | Tool-backed facts only + uncertainty labels | +| UC3 Pre-room checks | Meds, recent encounters, flags | Unsafe recommendation | Rule layer + “verify in room” phrasing | + +--- + +## Out of scope (for the sprint) + +- **Autonomous orders** or documentation without human action. +- **Cross-patient** analytics or population health (different user and trust model). +- **Generic medical chat** not tied to the **active patient’s** record. + +--- + +## Document control + +| Field | Value | +|--------|--------| +| **Project** | AgentForge — Clinical Co-Pilot | +| **Upstream fork** | [openemr/openemr](https://github.com/openemr/openemr) (`master`) | +| **Companion documents** | [AUDIT.md](AUDIT.md), [ARCHITECTURE.md](ARCHITECTURE.md) | +| **PRD alignment** | [ARCHITECTURE.md](ARCHITECTURE.md) must trace capabilities here; [AUDIT.md](AUDIT.md) informs integration risks. | From 00945939fbbc35045b35a86ad3092149f386c7bd Mon Sep 17 00:00:00 2001 From: Monica Peters Date: Tue, 28 Apr 2026 11:56:20 -0400 Subject: [PATCH 02/82] updated week 1 assignment docs --- ARCHITECTURE.md | 117 ++++++++++++++++++++++++++++++++++++++++-------- AUDIT.md | 12 ++++- USERS.md | 2 +- 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 529f0ddd9707..7efaeba06a21 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -39,10 +39,10 @@ Agent tools execute in-process under the authenticated OpenEMR session, delegati 8. [LLM integration (OpenAI)](#llm-integration-openai) 9. [Verification pipeline](#verification-pipeline) 10. [Observability options](#observability-options) -11. [Evaluation hooks](#evaluation-hooks) -12. [AI cost analysis (planning)](#ai-cost-analysis-planning) +11. [Evaluation hooks](#evaluation-hooks) ([dataset](#eval-dataset-canonical-description) · [run](#how-to-run-the-eval-suite) · [results](#results-submission-log)) +12. [AI cost analysis (planning)](#ai-cost-analysis-planning) ([measured spend](#measured-development-spend) · [projection assumptions](#projection-assumptions)) 13. [Failure modes and degradation](#failure-modes-and-degradation) -14. [Deployment (TBD checklist)](#deployment-tbd-checklist) +14. [Deployment (TBD checklist)](#deployment-tbd-checklist) ([Stage 1 local](#canonical-local-environment-stage-1) · [Stage 2 hosting](#target-hosting-stage-2-public-deploy)) 15. [References within this repository](#references-within-this-repository) --- @@ -57,14 +57,16 @@ Agent tools execute in-process under the authenticated OpenEMR session, delegati | PRD stage / gate | Where it is satisfied in-repo | |------------------|-------------------------------| +| Stage 1 — Run locally | [Canonical local environment (Stage 1)](#canonical-local-environment-stage-1) in this file; detailed commands in [CONTRIBUTING.md](CONTRIBUTING.md) / [CLAUDE.md](CLAUDE.md) | +| Stage 2 — Deploy | [Target hosting (Stage 2 public deploy)](#target-hosting-stage-2-public-deploy); canonical URL in [Deployment (TBD checklist)](#deployment-tbd-checklist) | | Stage 3 — Audit | [AUDIT.md](AUDIT.md) (five audit areas + ~500 word summary) | | Stage 4 — Users & use cases | [USERS.md](USERS.md) (narrow user + use cases + why conversational) | | Stage 5 — Agent integration plan | This file (~500 word summary + technical sections) | | Observability minimum questions | [Observability options](#observability-options) | -| Evaluation / test suite | [Evaluation hooks](#evaluation-hooks); detailed fixtures may live under `tests/` when code exists | +| Evaluation / test suite | [Evaluation hooks](#evaluation-hooks) (dataset, run instructions, [results log](#results-submission-log)); test code and fixtures under `tests/` when they exist | | Submitted deployed URL | [Deployment (TBD checklist)](#deployment-tbd-checklist) — canonical URL field | -**Course deliverables vs. these three files:** The PRD also names a fork **README** (setup guide), an **eval dataset with results**, and **AI cost analysis**. If instructors require those as **separate committed files**, add or extend them outside this trio and keep one-line pointers here. If not, treat the sections [Local setup and deployment pointers](#local-setup-and-deployment-pointers), [Evaluation hooks](#evaluation-hooks), and [AI cost analysis (planning)](#ai-cost-analysis-planning) as the in-repo record. +**Course deliverables vs. these three files:** The PRD also names a fork **README** (setup guide), an **eval dataset with results**, and **AI cost analysis**. **Eval dataset description, how to run the suite, submission results, and measured plus projected AI cost are canonical in this file** ([Evaluation hooks](#evaluation-hooks) and [AI cost analysis (planning)](#ai-cost-analysis-planning)). Keep the fork **README** short and point here for grading. If instructors require **additional** separate committed artifacts, add them outside this file and link one line from here. --- @@ -216,15 +218,67 @@ Modern requests may enter through [`public/index.php`](public/index.php), which **Mechanisms:** Unit tests for JSON schema and verification gate; integration tests with **mocked** OpenAI; synthetic fixtures only (**no real PHI**). -**Results log (update when runs exist):** Record date, command (e.g. `phpunit` target), pass/fail counts, and notable regressions in a bullet list here or in CI output linked from the fork. Until tests land, this subsection remains the **planned** eval contract. +### Eval dataset (canonical description) + +This subsection is the **single in-repo description** of what the eval suite is intended to cover (PRD: defensible, not only happy paths). Implementation lives under `tests/` when added; scenarios below map to the category table above. + +| Scenario group | Intent | Status | +|----------------|--------|--------| +| **Missing / incomplete chart** | Tool or model sees empty sections; output labels gaps, does not invent facts | TBD — wire tests when agent + tools exist | +| **Authorization / wrong patient** | Session A cannot retrieve patient B tool payloads; IDOR attempts fail closed | TBD | +| **Malformed model JSON** | Parser + one repair path; safe degradation | TBD | +| **Tool timeout / OpenAI error** | Explicit user-visible degradation; no silent invention | TBD | +| **Ambiguous user phrasing** | Clarification or conservative answer with uncertainty | TBD | +| **Golden tool JSON → safe UI text** | Given fixture tool output + model output, rendered text matches expected verified payload | TBD | + +All cases use **synthetic** chart and user strings only (**no real PHI**). + +### How to run the eval suite + +- **Isolated (host, no DB):** `composer phpunit-isolated` or `vendor/bin/phpunit -c phpunit-isolated.xml` — see [README-Isolated-Testing.md](README-Isolated-Testing.md). Use this for fast feedback on pure PHP components (e.g. schema and verification gate) as they land. +- **Full stack inside Docker:** From `docker/development-easy/`, use `/root/devtools` targets (e.g. `unit-test`, `services-test`) per [CLAUDE.md](CLAUDE.md) when tests require OpenEMR bootstrap, DB, or integration surfaces. + +Update this subsection when a dedicated agent/phpunit suite name exists (e.g. custom `phpunit.xml` group). + +### Results (submission log) + +Record every meaningful eval run you want graders to credit. Link to CI job URLs if results live primarily in GitHub Actions. + +| Date | Command | Pass / Fail | Notes | +|------|---------|---------------|-------| +| — | — | — | *Pending first run —* | --- ## AI cost analysis (planning) -**Development spend:** Track actual API spend during integration (model choice, average tools per request, tokens in/out). Update this subsection with rough monthly dev totals when available. +This section is the **canonical in-repo** place for **measured development spend**, **projection assumptions**, and **scale implications** (PRD submission: AI cost analysis). Update numbers after integration work; keep sources noted for auditability. + +### Measured development spend + +| Field | Value | +|-------|--------| +| **Date range** | — *(fill after first billing period)* | +| **Model(s)** | — *(e.g. gpt-4.x / reasoning tier)* | +| **Rough USD total** | — *(from vendor billing export or dashboard)* | +| **Data source** | — *(e.g. OpenAI usage page, export file name)* | +| **Notes** | Track average tools per request and tokens in/out when the agent loop is instrumented. | + +### Projection assumptions + +These variables feed order-of-magnitude cost thinking; **replace defaults with measured averages** from logs or observability once the agent is wired. -**Projected load (illustrative axes—replace with measured averages):** Assume *N* concurrent clinicians, *R* requests per clinician per hour, and *T* total input+output tokens per request (including tool JSON). Cost scales with *N × R × T ×* price-per-token; tool-heavy flows increase *T* faster than chat-only flows. +| Symbol | Meaning | Initial placeholder (revise with data) | +|--------|---------|----------------------------------------| +| *N* | Concurrent clinicians (or active sessions) | TBD | +| *R* | Agent requests per clinician per hour | TBD | +| *T* | Total input + output **tokens** per request (include tool JSON and verification passes) | TBD; tool-heavy flows grow *T* faster than chat-only | + +Cost scales roughly with *N × R × T ×* price-per-token; **bounded tools**, **verification passes**, and **caching policy** dominate at scale—this is **not** “cost-per-token × users” alone. + +### Illustrative scale table (architectural implications, not measured billing) + +The following table is **not** a bill forecast; it records **engineering responses** at different adoption levels given the assumptions above. | Scale (active clinical users) | Architectural implication | |------------------------------|----------------------------| @@ -233,8 +287,6 @@ Modern requests may enter through [`public/index.php`](public/index.php), which | **~10K** | Regional deployment, aggressive **minimum-necessary** prompts, model tiering (smaller model for routing), batch where safe—not linear “token × users” without redesign. | | **~100K** | Dedicated inference contracts, possible **on-VPC** or Azure OpenAI–class deployments; observability and cost allocation per site/department. | -This is **not** “cost-per-token × users” alone; bounded tools, verification passes, and caching policy dominate at scale. - --- ## Failure modes and degradation @@ -253,19 +305,46 @@ This is **not** “cost-per-token × users” alone; bounded tools, verification **Canonical deployed application URL (PRD submissions):** `TBD` — replace at each checkpoint (MVP, Early Submission, Final) with the **publicly reachable** URL of this fork’s deployment; keep in sync with what you submit to the course. -**Hosting not yet selected.** Before going live even with synthetic data, complete: +### Target hosting (Stage 2 public deploy) + +**Provider:** **Railway + Docker deployment** (public PaaS runtime for this sprint). Keep an optional **Environment label** line here when you want a named deployment (for example `staging` or `prd-week1`) on record. + +**Topology:** + +- **Compute:** Railway service running the OpenEMR container image (Dockerfile-based deployment). +- **Services:** Dockerized OpenEMR application plus MariaDB dependency in Railway (managed database service or attached containerized DB service, whichever is used by this fork). +- **Networking/TLS:** Public Railway URL with platform HTTPS termination; keep OpenEMR app traffic HTTPS-only and avoid exposing plaintext-only endpoints in production routing. +- **Configuration:** Environment variables and API keys are injected through Railway project/service settings, never committed to the repository. +- **Scope:** Single-region, single-environment baseline for Week 1 (no HA or multi-region claim unless explicitly extended). -- [ ] TLS certificate and HTTPS-only cookies -- [ ] Secrets management (API keys) +**Alignment with local:** Stage 1 uses **Easy Development Docker** under [`docker/development-easy/`](docker/development-easy/); Stage 2 uses the same Dockerized OpenEMR stack in the **Railway + Docker deployment** so PHP, MariaDB, and OpenEMR behavior stay comparable between laptop and hosted environment. See [DOCKER_README.md](DOCKER_README.md) for the broader Docker layout. + +**Runtime (pin when live):** PHP version and extensions should match [CLAUDE.md](CLAUDE.md) expectations (PHP **8.2+** for modern OpenEMR development); list any fork-specific `docker-compose` overrides in a bullet below once stable. + +Before going live even with synthetic data, complete: + +- [ ] Confirm Railway HTTPS endpoint, cookie security flags, and HTTPS-only access +- [ ] Secrets management via Railway variables (API keys, DB credentials) - [ ] Admin password rotation / demo banner -- [ ] Log rotation and **redaction** rules -- [ ] Backup policy for DB (if storing traces) -- [ ] Rollback: prior container image or release tag +- [ ] Platform and app log **redaction** rules (including prompt/tool payload controls) +- [ ] Backup/restore policy for DB and any persisted volumes in the Railway + Docker deployment +- [ ] Rollback/redeploy path (previous container image or release config) + +### Canonical local environment (Stage 1) + +**Canonical path** for “run OpenEMR locally” in this fork is the **Easy Development Docker** stack under [`docker/development-easy/`](docker/development-easy/), started per [CONTRIBUTING.md — Code contributions (local development)](CONTRIBUTING.md#code-contributions-local-development) and summarized in [CLAUDE.md](CLAUDE.md): after `docker compose up --detach --wait` from that directory, the app is at **http://localhost:8300/** or **https://localhost:9300/**; default login **admin** / **pass**; phpMyAdmin at **http://localhost:8310/** when that service is part of the compose profile. Load **realistic sample patient data** for demos (PRD Stage 1)—**synthetic / demo only; never production PHI**. + +**Pointers (commands and CI expectations live in these files—avoid duplicating long runbooks here):** + +- [CONTRIBUTING.md](CONTRIBUTING.md) — local development and contribution workflow +- [DOCKER_README.md](DOCKER_README.md) — production vs development Docker families +- [CLAUDE.md](CLAUDE.md) — URLs, credentials summary, and `docker compose exec openemr /root/devtools` test entrypoints +- [README-Isolated-Testing.md](README-Isolated-Testing.md) — host-only PHPUnit (`composer phpunit-isolated`) for isolated suites when logging eval runs without a full DB +- Upstream reference: [openemr/openemr](https://github.com/openemr/openemr) for stack changes outside this fork -### Local setup and deployment pointers +**Fork-specific compose/env overrides (fill as you stabilize):** -- **Local OpenEMR:** Follow upstream guidance (e.g. Docker or stack docs from [openemr/openemr](https://github.com/openemr/openemr)); fork-specific env vars or compose overrides should be **documented here in bullet form** as you stabilize them so Week 1 “Run it locally” stays traceable in-repo. -- **Public deploy:** Same stack family as intended for the final agent reduces surprise; record provider and **runtime** (PHP version, extensions) briefly here once chosen. +- *(None documented yet—add bullets here for env vars or compose file paths unique to this fork.)* --- diff --git a/AUDIT.md b/AUDIT.md index 5072eac5f6a3..4d85d15cb693 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -15,7 +15,7 @@ For **HIPAA-aligned design**, the critical dimensions are **where PHI lives** (d **Data quality** is uneven in any real EHR: duplicate problems, stale meds, scanned PDFs versus structured entries, and unsigned notes. **Agent failure modes** mirror these gaps; verification must treat **missing** and **ambiguous** as first-class outcomes. -**Compliance:** Class deployment uses **synthetic chart text** and may call **OpenAI’s API**, but **production PHI** would require **Business Associate Agreements**, **minimum necessary** payloads, **training / retention** guarantees, subprocessors review, and likely **Azure OpenAI** or equivalent enterprise contracts. **Public observability SaaS** is **high risk** for PHI unless **redacted** or **self-hosted**; default recommendation is **server-side redacted logs** (see [ARCHITECTURE.md](ARCHITECTURE.md)). +**Compliance:** Class deployment uses **synthetic chart text** and may call **OpenAI’s API**, with the Week 1 runtime using a **Railway + Docker deployment**. For **production PHI**, this would still require **Business Associate Agreements**, **minimum necessary** payloads, **training / retention** guarantees, subprocessors review, and likely **Azure OpenAI** or equivalent enterprise contracts. **Public observability SaaS** remains **high risk** for PHI unless redacted; for the Railway + Docker deployment, maintain **server-side redacted logs** and explicit retention controls (see [ARCHITECTURE.md](ARCHITECTURE.md)). **Bottom line:** OpenEMR is a **credible foundation** for a **session-bound, in-process** Clinical Co-Pilot that **reuses existing authorization layers** and limits LLM exposure to **necessary structured excerpts**. The highest-impact audit outcome is **not** “avoid OpenEMR,” but **control integration surface area**: strict tool contracts, mandatory verification, **no PHI in third-party traces**, and a documented **upgrade path** to OAuth2-scoped APIs before real clinical deployment. @@ -54,6 +54,7 @@ For **HIPAA-aligned design**, the critical dimensions are **where PHI lives** (d - **PHP error logs** (stack traces can include SQL or paths). - **LLM vendor logs** (policy-dependent; assume sensitive). - **Observability third parties** (default: **do not send** raw prompts/responses). +- **Platform logs on Railway** (treat as sensitive operational telemetry; keep PHI out of log lines). ### PHI handling gaps (inherent to integration, not solely OpenEMR) @@ -141,6 +142,13 @@ flowchart LR - If logs contain **PHI**, they become **high-sensitivity assets** with retention limits. - Sprint approach: **synthetic PHI only**; logs **redacted**. +- In the Railway + Docker deployment, document the ownership split: app-level redaction is your responsibility; platform log access, retention windows, and export paths must be explicitly reviewed and configured. + +### Railway operational controls (Week 1 hosting model) + +- Keep all secrets in Railway environment/service variables; never commit secrets or bake them into container images. +- Pin deployment region deliberately and verify data-flow implications for external API egress. +- Define backup/restore accountability for DB and persisted application storage before any non-demo usage. ### BAA assumption (per PRD) @@ -152,7 +160,7 @@ Gauntlet Week 1 PRD: act **as if** a **Business Associate Agreement** (or equiva | ID | Severity | Finding | Recommendation | |----|----------|---------|----------------| -| F1 | High | LLM + observability can **exfiltrate PHI** via logs | Redact; prefer self-hosted or DB-stored redacted traces | +| F1 | High | LLM + observability can **exfiltrate PHI** via logs (including platform logs) | Redact aggressively; prefer in-app redacted traces and strict platform log controls | | F2 | High | New agent endpoints could **skip ACL** if rushed | Central middleware enforcing patient + permission | | F3 | Medium | Legacy surface increases **audit inconsistency** | Reuse `src/Services` patterns; add tests | | F4 | Medium | Data quality causes **hallucination-like** failures | Verification + “missing data” UX | diff --git a/USERS.md b/USERS.md index de7017975b44..86e8e4ddf031 100644 --- a/USERS.md +++ b/USERS.md @@ -12,7 +12,7 @@ This document is the **source of truth** for *who* the Clinical Co-Pilot serves **Setting:** Community **outpatient clinic** with a **high-volume schedule** (approximately fifteen to twenty-five face-to-face visits per day), mixed acute and chronic care, plus inbox and results tasks between visits. -**Technical context:** The physician already uses **OpenEMR** for scheduling, charting, e-prescribing, and lab/imaging review. They move between exam rooms with **roughly one minute or less** between patients to re-orient on the next chart. +**Technical context:** The physician already uses **OpenEMR** for scheduling, charting, e-prescribing, and lab/imaging review; for this Week 1 project, that OpenEMR environment is delivered via a **Railway + Docker deployment**. They move between exam rooms with **roughly one minute or less** between patients to re-orient on the next chart. **Goals:** Minimize cognitive load, avoid missing important changes since the last visit, and enter the room with an accurate mental model without reading the entire chart. From e36e18fab70693e1fe754004e3e4cf0380c4b6b5 Mon Sep 17 00:00:00 2001 From: Monica Peters Date: Wed, 29 Apr 2026 19:33:43 -0400 Subject: [PATCH 03/82] feat(clinical-copilot): add AgentForge custom module and eval tests --- ARCHITECTURE.md | 42 ++--- composer.json | 6 +- .../oe-module-clinical-copilot/README.md | 34 +++++ .../oe-module-clinical-copilot/info.txt | 1 + .../moduleConfig.php | 35 +++++ .../openemr.bootstrap.php | 23 +++ .../public/copilot_request.php | 17 +++ .../src/Bootstrap.php | 94 ++++++++++++ .../src/ClinicalCopilotCard.php | 55 +++++++ .../Controller/CopilotRequestController.php | 99 ++++++++++++ .../src/Services/AgentOrchestrator.php | 127 +++++++++++++++ .../src/Services/AgentTelemetry.php | 67 ++++++++ .../src/Services/ChartContextTool.php | 68 +++++++++ .../src/Services/OpenAiClient.php | 105 +++++++++++++ .../src/Services/VerificationGate.php | 144 ++++++++++++++++++ .../clinical_copilot/summary_card.html.twig | 45 ++++++ .../oe-module-clinical-copilot/version.php | 14 ++ .../VerificationGateIsolatedTest.php | 83 ++++++++++ 18 files changed, 1037 insertions(+), 22 deletions(-) create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/README.md create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/info.txt create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig create mode 100644 interface/modules/custom_modules/oe-module-clinical-copilot/version.php create mode 100644 tests/Tests/Isolated/ClinicalCopilot/VerificationGateIsolatedTest.php diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7efaeba06a21..d7dc10db376e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -49,7 +49,7 @@ Agent tools execute in-process under the authenticated OpenEMR session, delegati ## Repository scope (Week 1 / PRD) -**Single source of truth in this fork:** Week 1 AgentForge planning, hard-gate documentation, deployment URL for submissions, and summaries of eval intent and AI cost **live in this repository**—primarily in [AUDIT.md](AUDIT.md), [USERS.md](USERS.md), and this file—alongside [`Documentation/PRD_Week1_AgentForge.pdf`](Documentation/PRD_Week1_AgentForge.pdf). Avoid parallel specs in external tools that can drift from what graders see in GitHub. +**Single source of truth in this fork:** Week 1 AgentForge planning, hard-gate documentation, deployment URL for submissions, and summaries of eval intent and AI cost **live in this repository**—primarily in [AUDIT.md](AUDIT.md), [USERS.md](USERS.md), and this file—alongside [`PRD_Week1_AgentForge.md`](PRD_Week1_AgentForge.md). Avoid parallel specs in external tools that can drift from what graders see in GitHub. **Capability boundary:** [USERS.md](USERS.md) defines the **only** user problems the agent may address; every tool, prompt, and UI behavior in later implementation must **trace to a use case** there. The audit ([AUDIT.md](AUDIT.md)) is the **input** to this architecture plan (PRD Stage 5); implementation should not outrun audited risks. @@ -229,13 +229,13 @@ This subsection is the **single in-repo description** of what the eval suite is | **Malformed model JSON** | Parser + one repair path; safe degradation | TBD | | **Tool timeout / OpenAI error** | Explicit user-visible degradation; no silent invention | TBD | | **Ambiguous user phrasing** | Clarification or conservative answer with uncertainty | TBD | -| **Golden tool JSON → safe UI text** | Given fixture tool output + model output, rendered text matches expected verified payload | TBD | +| **Golden tool JSON → safe UI text** | Given fixture tool output + model output, rendered text matches expected verified payload | Partial — `VerificationGateIsolatedTest` covers citation path resolution and stripping | All cases use **synthetic** chart and user strings only (**no real PHI**). ### How to run the eval suite -- **Isolated (host, no DB):** `composer phpunit-isolated` or `vendor/bin/phpunit -c phpunit-isolated.xml` — see [README-Isolated-Testing.md](README-Isolated-Testing.md). Use this for fast feedback on pure PHP components (e.g. schema and verification gate) as they land. +- **Isolated (host, no DB):** `composer dump-autoload -o` then `composer phpunit-isolated -- --filter ClinicalCopilot` (or `vendor/bin/phpunit -c phpunit-isolated.xml --filter ClinicalCopilot`) — see [README-Isolated-Testing.md](README-Isolated-Testing.md). Covers citation verification for the Clinical Co-Pilot module (`tests/Tests/Isolated/ClinicalCopilot/`). - **Full stack inside Docker:** From `docker/development-easy/`, use `/root/devtools` targets (e.g. `unit-test`, `services-test`) per [CLAUDE.md](CLAUDE.md) when tests require OpenEMR bootstrap, DB, or integration surfaces. Update this subsection when a dedicated agent/phpunit suite name exists (e.g. custom `phpunit.xml` group). @@ -246,7 +246,7 @@ Record every meaningful eval run you want graders to credit. Link to CI job URLs | Date | Command | Pass / Fail | Notes | |------|---------|---------------|-------| -| — | — | — | *Pending first run —* | +| 2026-04-29 | `composer phpunit-isolated -- --filter ClinicalCopilot` | *(run locally / CI)* | Verifies `VerificationGate` citation stripping for `oe-module-clinical-copilot` | --- @@ -303,32 +303,34 @@ The following table is **not** a bill forecast; it records **engineering respons ## Deployment (TBD checklist) **Canonical deployed application URL (PRD submissions):** -`TBD` — replace at each checkpoint (MVP, Early Submission, Final) with the **publicly reachable** URL of this fork’s deployment; keep in sync with what you submit to the course. +`https://openemr-210925-0.cloudclusters.net/` — Cloud Clusters managed OpenEMR (Week 1 AgentForge); keep in sync with course submission forms. ### Target hosting (Stage 2 public deploy) -**Provider:** **Railway + Docker deployment** (public PaaS runtime for this sprint). Keep an optional **Environment label** line here when you want a named deployment (for example `staging` or `prd-week1`) on record. +**Provider:** **[Cloud Clusters](https://www.cloudclusters.io/cloud/openemr)** — **OpenEMR Docker** managed hosting (not Railway). Rationale: many current OpenEMR users and practices already run on or are familiar with this class of **managed, Docker-based** OpenEMR hosting, which supports trust and operational expectations for demos and SMB-style deployments. + +**Vendor positioning (marketing summary):** OpenEMR is described as a widely used open-source EHR and practice-management stack; Cloud Clusters advertises **easy deployments**, simplified management, **high network security**, reliability, and uptime, with entry pricing around **$4.99/mo** and a **free demo** path. Plan highlights from their OpenEMR product page: **SMB-friendly**, **managed cloud**, **OpenEMR 7.0.1 Community**, stack **Ubuntu + MySQL 8.0 + PHP 7.4 + Apache 2.4** (confirm the **live** image/version and PHP runtime in your control panel after provisioning—vendor pages can lag upstream `master`, and local Easy Docker often uses **newer PHP**; validate agent and OpenEMR compatibility on the **actual** hosted stack). **Topology:** -- **Compute:** Railway service running the OpenEMR container image (Dockerfile-based deployment). -- **Services:** Dockerized OpenEMR application plus MariaDB dependency in Railway (managed database service or attached containerized DB service, whichever is used by this fork). -- **Networking/TLS:** Public Railway URL with platform HTTPS termination; keep OpenEMR app traffic HTTPS-only and avoid exposing plaintext-only endpoints in production routing. -- **Configuration:** Environment variables and API keys are injected through Railway project/service settings, never committed to the repository. -- **Scope:** Single-region, single-environment baseline for Week 1 (no HA or multi-region claim unless explicitly extended). +- **Compute:** Managed OpenEMR **Docker** instance on Cloud Clusters’ platform (isolated resources per tenant per their documentation). +- **Data / DB:** **MySQL 8.0** per vendor environment description; backups and on-demand restore advertised as control-panel features. +- **Networking/TLS:** Public HTTPS URL with **free SSL** per vendor; keep OpenEMR cookies and admin URLs on HTTPS-only paths. +- **Configuration:** Admin password, DNS, SSL, and any **OpenAI API keys** for the agent via the **Cloud Clusters control panel** (and OpenEMR globals)—**never** commit secrets to the repository. +- **Scope:** Single-environment Week 1 baseline (no multi-region / HA claim unless you extend it). -**Alignment with local:** Stage 1 uses **Easy Development Docker** under [`docker/development-easy/`](docker/development-easy/); Stage 2 uses the same Dockerized OpenEMR stack in the **Railway + Docker deployment** so PHP, MariaDB, and OpenEMR behavior stay comparable between laptop and hosted environment. See [DOCKER_README.md](DOCKER_README.md) for the broader Docker layout. +**Alignment with local:** Stage 1 remains **Easy Development Docker** under [`docker/development-easy/`](docker/development-easy/). Stage 2 is **Cloud Clusters** managed OpenEMR, so behavior is “same product family, possibly different PHP/compose details”—**re-test** the agent and co-pilot paths on the hosted URL after deploy. See [DOCKER_README.md](DOCKER_README.md) for local Docker layout. -**Runtime (pin when live):** PHP version and extensions should match [CLAUDE.md](CLAUDE.md) expectations (PHP **8.2+** for modern OpenEMR development); list any fork-specific `docker-compose` overrides in a bullet below once stable. +**Runtime (pin when live):** Record the **actual** PHP and OpenEMR versions shown in Cloud Clusters after install; compare to [CLAUDE.md](CLAUDE.md) / [CONTRIBUTING.md](CONTRIBUTING.md) local flex image if you hit extension or version skew. Before going live even with synthetic data, complete: -- [ ] Confirm Railway HTTPS endpoint, cookie security flags, and HTTPS-only access -- [ ] Secrets management via Railway variables (API keys, DB credentials) +- [ ] Confirm Cloud Clusters HTTPS endpoint, cookie security flags, and HTTPS-only access +- [ ] Secrets and API keys only in control panel / OpenEMR secured globals—never in git - [ ] Admin password rotation / demo banner -- [ ] Platform and app log **redaction** rules (including prompt/tool payload controls) -- [ ] Backup/restore policy for DB and any persisted volumes in the Railway + Docker deployment -- [ ] Rollback/redeploy path (previous container image or release config) +- [ ] Platform and app log **redaction** rules (including prompt/tool payload controls); review host **WAF** / platform logging if enabled +- [ ] Backup/restore policy using Cloud Clusters backup features + OpenEMR export posture +- [ ] Rollback/redeploy path (snapshot, plan downgrade, or redeploy from vendor workflow) ### Canonical local environment (Stage 1) @@ -358,7 +360,7 @@ Before going live even with synthetic data, complete: | API / OAuth / FHIR (future path) | [`Documentation/api/`](Documentation/api/) | | Contributor / quality bar | [`CLAUDE.md`](CLAUDE.md) | | Security reporting | [`.github/SECURITY.md`](.github/SECURITY.md) | -| PRD | [`Documentation/PRD_Week1_AgentForge.pdf`](Documentation/PRD_Week1_AgentForge.pdf) | +| PRD | [`PRD_Week1_AgentForge.md`](PRD_Week1_AgentForge.md) | --- @@ -368,4 +370,4 @@ Before going live even with synthetic data, complete: |--------|--------| | **Project** | AgentForge — Clinical Co-Pilot | | **Companion documents** | [USERS.md](USERS.md), [AUDIT.md](AUDIT.md) | -| **PRD (Week 1)** | [`Documentation/PRD_Week1_AgentForge.pdf`](Documentation/PRD_Week1_AgentForge.pdf) — submission table lists `./USER.md`; this fork uses **`./USERS.md`** at repo root (align with graders if needed). | +| **PRD (Week 1)** | [`PRD_Week1_AgentForge.md`](PRD_Week1_AgentForge.md) — submission table lists `./USER.md`; this fork uses **`./USERS.md`** at repo root (align with graders if needed). | diff --git a/composer.json b/composer.json index 2d5a375f22f3..8415c1f5cf65 100644 --- a/composer.json +++ b/composer.json @@ -164,7 +164,8 @@ ], "autoload": { "psr-4": { - "OpenEMR\\": "src" + "OpenEMR\\": "src", + "OpenEMR\\Modules\\ClinicalCopilot\\": "interface/modules/custom_modules/oe-module-clinical-copilot/src" }, "classmap": [ "library/classes" @@ -234,7 +235,8 @@ "ext-zlib": "8.2", "php": "8.2" }, - "sort-packages": true + "sort-packages": true, + "process-timeout": 0 }, "extra": { "composer-normalize": { diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/README.md b/interface/modules/custom_modules/oe-module-clinical-copilot/README.md new file mode 100644 index 000000000000..5d0ca476838f --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/README.md @@ -0,0 +1,34 @@ +# Clinical Co-Pilot (`oe-module-clinical-copilot`) + +AgentForge Week 1 module: **inter-visit briefing** on the **patient summary** (primary dashboard column) using OpenAI, **citation-backed** statements, **structured telemetry**, and **PHPUnit-isolated** verification tests. + +## Install (OpenEMR Admin) + +1. Copy or merge this folder to `interface/modules/custom_modules/oe-module-clinical-copilot/`. +2. **Administration → System → Modules** → install **Clinical Co-Pilot (AgentForge)** → **Enable**. +3. Open a patient and go to the **patient summary / dashboard**; the **Clinical Co-Pilot** card should appear at the top of the primary column. + +## Configuration + +- **Enable:** Globals metadata from `moduleConfig.php` — `clinical_copilot_enable` (default on). +- **Model:** `clinical_copilot_openai_model` (default `gpt-4o-mini`). +- **OpenAI API key (pick one):** + - Environment: `CLINICAL_COPILOT_OPENAI_API_KEY` or `OPENAI_API_KEY` (preferred on [Cloud Clusters](https://www.cloudclusters.io/) and Docker), or + - **Administration → Globals → Portal** → **Clinical Co-Pilot OpenAI API key** (password field). + +Use **demo / synthetic data only** per course rules. + +## Eval / tests + +```bash +composer dump-autoload -o +composer phpunit-isolated -- --filter ClinicalCopilot +``` + +## Architecture notes + +- **Trust boundary:** Session `pid` only; AJAX ignores client-supplied patient id. +- **Verification:** Model must return JSON `statements` with `citations` as dot paths into chart tool JSON; uncited lines are stripped server-side. +- **Telemetry:** Monolog channel with JSON payload: steps, token counts, rough USD estimate (not a bill). + +See repo root `ARCHITECTURE.md` and `USERS.md` for full AgentForge context. diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/info.txt b/interface/modules/custom_modules/oe-module-clinical-copilot/info.txt new file mode 100644 index 000000000000..6d1c8a81ad0f --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/info.txt @@ -0,0 +1 @@ +Clinical Co-Pilot (AgentForge) v0.1.0 — conversational briefing on patient summary with verification, telemetry, and OpenAI. diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php b/interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php new file mode 100644 index 000000000000..c98161dc22f3 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php @@ -0,0 +1,35 @@ + 'Clinical Co-Pilot (AgentForge)', + 'description' => 'AI-assisted inter-visit briefing on the patient dashboard with citation-backed output, eval hooks, and structured telemetry.', + 'version' => '0.1.0', + 'author' => 'AgentForge fork', + 'license' => 'GPL-3.0', + 'acl_category' => 'patients', + 'acl_section' => 'demo', + 'require' => [ + 'openemr' => '>=7.0.0', + ], + 'globals' => [ + [ + 'name' => 'clinical_copilot_enable', + 'type' => 'bool', + 'default' => '1', + 'description' => 'Enable Clinical Co-Pilot card on patient summary', + ], + [ + 'name' => 'clinical_copilot_openai_model', + 'type' => 'text', + 'default' => 'gpt-4o-mini', + 'description' => 'OpenAI chat model id (e.g. gpt-4o-mini)', + ], + ], +]; diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php b/interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php new file mode 100644 index 000000000000..425d5dd1301a --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php @@ -0,0 +1,23 @@ +registerNamespaceIfNotExists( + 'OpenEMR\\Modules\\ClinicalCopilot\\', + __DIR__ . DIRECTORY_SEPARATOR . 'src' +); + +$bootstrap = new Bootstrap($eventDispatcher); +$bootstrap->subscribeToEvents(); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php b/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php new file mode 100644 index 000000000000..03f86b186592 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php @@ -0,0 +1,17 @@ +handle(); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php new file mode 100644 index 000000000000..b90d4b8115ad --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php @@ -0,0 +1,94 @@ +moduleDir = dirname(__DIR__); + } + + public function subscribeToEvents(): void + { + $this->eventDispatcher->addListener(SectionEvent::EVENT_HANDLE, $this->addCopilotCard(...), 100); + $this->eventDispatcher->addListener(TwigEnvironmentEvent::EVENT_CREATED, $this->addTwigPath(...)); + $this->eventDispatcher->addListener(GlobalsInitializedEvent::EVENT_HANDLE, $this->registerGlobals(...)); + } + + public function addCopilotCard(SectionEvent $event): void + { + if ($event->getSection() !== 'primary') { + return; + } + if (!OEGlobalsBag::getInstance()->getBoolean('clinical_copilot_enable', true)) { + return; + } + try { + $event->addCard(new ClinicalCopilotCard(), 0); + } catch (\Throwable $e) { + // Duplicate identifier if listener ever double-fired + if (str_contains($e->getMessage(), 'not unique')) { + return; + } + throw $e; + } + } + + public function addTwigPath(TwigEnvironmentEvent $event): void + { + try { + $twig = $event->getTwigEnvironment(); + $loader = $twig->getLoader(); + if ($loader instanceof FilesystemLoader) { + $loader->prependPath($this->moduleDir . DIRECTORY_SEPARATOR . 'templates'); + } + } catch (LoaderError $e) { + // Non-fatal: card render would fail without templates; log once + error_log('ClinicalCopilot: twig path ' . $e->getMessage()); + } + } + + public function registerGlobals(GlobalsInitializedEvent $event): void + { + $service = $event->getGlobalsService(); + $meta = $service->getGlobalsMetadata(); + if (!isset($meta['Portal'])) { + return; + } + if (isset($meta['Portal']['clinical_copilot_openai_api_key'])) { + return; + } + $service->appendToSection( + 'Portal', + 'clinical_copilot_openai_api_key', + new GlobalSetting( + xl('Clinical Co-Pilot OpenAI API key'), + GlobalSetting::DATA_TYPE_PASS, + '', + xl('Optional if CLINICAL_COPILOT_OPENAI_API_KEY or OPENAI_API_KEY is set in the server environment.'), + false + ) + ); + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php new file mode 100644 index 000000000000..99e0310b021d --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php @@ -0,0 +1,55 @@ + ['patients', 'demo'], + 'initiallyCollapsed' => false, + 'add' => false, + 'edit' => false, + 'collapse' => true, + 'templateFile' => self::TEMPLATE, + 'identifier' => 'clinical_copilot', + 'title' => xl('Clinical Co-Pilot'), + ]); + } + + /** + * @return array + */ + public function getTemplateVariables(): array + { + $session = SessionWrapperFactory::getInstance()->getActiveSession(); + $csrf = CsrfUtils::collectCsrfToken($session, 'default'); + $webroot = OEGlobalsBag::getInstance()->getWebRoot(); + $pid = (int) ($session->get('pid') ?? 0); + + return [ + 'card' => $this, + 'auth' => false, + 'copilotAjaxUrl' => $webroot . '/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php', + 'csrf' => $csrf, + 'pid' => $pid, + ]; + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php new file mode 100644 index 000000000000..3efca61eeda7 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php @@ -0,0 +1,99 @@ +getActiveSession(); + if ($session->get('authUser') === null || $session->get('authUser') === '') { + http_response_code(401); + echo json_encode(['ok' => false, 'error' => 'not_logged_in']); + return; + } + + try { + CsrfUtils::checkCsrfInput(INPUT_POST, $session, 'csrf_token_form', 'default', false); + } catch (\OpenEMR\Common\Csrf\CsrfInvalidException $e) { + http_response_code(403); + echo json_encode(['ok' => false, 'error' => 'csrf']); + return; + } + + if (!AclMain::aclCheckCore('patients', 'demo')) { + http_response_code(403); + echo json_encode(['ok' => false, 'error' => 'acl']); + return; + } + + $pid = (int) ($session->get('pid') ?? 0); + // Ignore any client-supplied pid (IDOR hardening) + $requestId = bin2hex(random_bytes(8)); + + $apiKey = $this->resolveApiKey(); + $orchestrator = new AgentOrchestrator( + new ChartContextTool(), + new OpenAiClient($apiKey), + new VerificationGate() + ); + + $telemetry = new AgentTelemetry(); + $result = $orchestrator->runBriefing($pid, $telemetry, $requestId); + + if (!($result['ok'] ?? false)) { + $code = ($result['error'] ?? '') === 'not_logged_in' ? 401 : 200; + http_response_code($code); + echo json_encode([ + 'ok' => false, + 'error' => $result['error'] ?? 'unknown', + 'request_id' => $requestId, + ]); + return; + } + + echo json_encode([ + 'ok' => true, + 'text' => $result['text'] ?? '', + 'request_id' => $requestId, + 'usage' => $result['usage'] ?? [], + 'model' => $result['model'] ?? '', + 'estimated_usd' => $result['estimated_usd'] ?? 0.0, + ]); + } + + private function resolveApiKey(): ?string + { + $fromEnv = getenv('CLINICAL_COPILOT_OPENAI_API_KEY'); + if (is_string($fromEnv) && $fromEnv !== '') { + return $fromEnv; + } + $fromEnv2 = getenv('OPENAI_API_KEY'); + if (is_string($fromEnv2) && $fromEnv2 !== '') { + return $fromEnv2; + } + $g = OEGlobalsBag::getInstance()->getString('clinical_copilot_openai_api_key'); + return ($g !== '') ? $g : null; + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php new file mode 100644 index 000000000000..c0f41c4ca6b0 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php @@ -0,0 +1,127 @@ +,model?:string,estimated_usd?:float} + */ + public function runBriefing(int $sessionPid, AgentTelemetry $telemetry, string $requestId): array + { + $telemetry->mark('start', true); + if ($sessionPid < 1) { + $telemetry->mark('pid_check', false, 'no active patient'); + $telemetry->flush($requestId, $sessionPid); + return ['ok' => false, 'error' => 'no_active_patient', 'telemetry' => $telemetry]; + } + + try { + $toolData = $this->chartTool->collectForPatient($sessionPid); + $telemetry->mark('tool_chart_context', true); + } catch (\Throwable $e) { + $telemetry->mark('tool_chart_context', false, $e->getMessage()); + $telemetry->flush($requestId, $sessionPid); + return ['ok' => false, 'error' => 'tool_failure', 'telemetry' => $telemetry]; + } + + $model = OEGlobalsBag::getInstance()->getString('clinical_copilot_openai_model') ?: 'gpt-4o-mini'; + if (!$this->openAi->hasApiKey()) { + $telemetry->mark('openai', false, 'missing_api_key'); + $telemetry->flush($requestId, $sessionPid); + return ['ok' => false, 'error' => 'missing_openai_api_key', 'telemetry' => $telemetry]; + } + + $toolJson = json_encode($toolData, JSON_UNESCAPED_SLASHES); + if ($toolJson === false) { + $telemetry->mark('encode_tool', false); + $telemetry->flush($requestId, $sessionPid); + return ['ok' => false, 'error' => 'encode_failure', 'telemetry' => $telemetry]; + } + + $system = << 'system', 'content' => $system], + ['role' => 'user', 'content' => "CHART_JSON:\n" . $toolJson . "\n\nTask: one-paragraph room briefing: who this is, why they may be here if inferable from problems only as possibilities labeled uncertain, allergies, key meds. Use citations."], + ]; + + try { + $t0 = microtime(true); + $resp = $this->openAi->chatJson($model, $messages); + $telemetry->mark('openai', true, 'latency_ms=' . round((microtime(true) - $t0) * 1000)); + } catch (\Throwable $e) { + $telemetry->mark('openai', false, $e->getMessage()); + $telemetry->flush($requestId, $sessionPid, []); + return ['ok' => false, 'error' => 'openai_failure', 'telemetry' => $telemetry]; + } + + $parsed = json_decode($resp['content'], true); + if (!is_array($parsed)) { + $telemetry->mark('parse_model_json', false); + $telemetry->flush($requestId, $sessionPid, $this->usageContext($resp)); + return ['ok' => false, 'error' => 'malformed_model_json', 'telemetry' => $telemetry]; + } + $telemetry->mark('parse_model_json', true); + + $verified = $this->verification->verify($toolData, $parsed); + $telemetry->mark('verification', true, 'kept=' . count($verified['statements']) . ',stripped=' . count($verified['stripped'])); + $text = $this->verification->formatForDisplay($verified); + + $usageCtx = $this->usageContext($resp); + $telemetry->flush($requestId, $sessionPid, $usageCtx); + + return [ + 'ok' => true, + 'text' => $text, + 'verified' => $verified, + 'telemetry' => $telemetry, + 'usage' => $resp['usage'], + 'model' => $resp['model'], + 'estimated_usd' => OpenAiClient::estimateCostUsd($resp['model'], $resp['usage']), + ]; + } + + /** + * @param array{usage:array,model:string} $resp + * @return array + */ + private function usageContext(array $resp): array + { + $u = $resp['usage']; + return [ + 'prompt_tokens' => $u['prompt_tokens'] ?? 0, + 'completion_tokens' => $u['completion_tokens'] ?? 0, + 'total_tokens' => $u['total_tokens'] ?? 0, + 'model' => $resp['model'], + 'estimated_usd' => OpenAiClient::estimateCostUsd($resp['model'], $u), + ]; + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php new file mode 100644 index 000000000000..94c15050af2f --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php @@ -0,0 +1,67 @@ + */ + private array $steps = []; + + private float $t0; + + public function __construct() + { + $this->t0 = microtime(true); + } + + public function mark(string $step, bool $ok = true, ?string $detail = null): void + { + $this->steps[] = [ + 'step' => $step, + 'ms' => round((microtime(true) - $this->t0) * 1000, 2), + 'ok' => $ok, + 'detail' => $detail !== null ? $this->redact($detail) : null, + ]; + } + + /** + * @param array $context + */ + public function flush(string $requestId, int $pid, array $context = []): void + { + $payload = [ + 'component' => 'clinical_copilot', + 'request_id' => $requestId, + 'pid_present' => $pid > 0, + 'steps' => $this->steps, + ]; + foreach ($context as $k => $v) { + if ($k === 'prompt_tokens' || $k === 'completion_tokens' || $k === 'total_tokens' || $k === 'model' || $k === 'estimated_usd') { + $payload[$k] = $v; + } + } + $line = json_encode($payload, JSON_UNESCAPED_SLASHES); + if ($line !== false) { + ServiceContainer::getLogger()->info('clinical_copilot_telemetry', ['json' => $line]); + } + } + + private function redact(string $s): string + { + if (strlen($s) > 500) { + $s = substr($s, 0, 500) . '…'; + } + return $s; + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php new file mode 100644 index 000000000000..549ebe308621 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php @@ -0,0 +1,68 @@ + + */ + public function collectForPatient(int $pid): array + { + if ($pid < 1) { + return ['patient' => [], 'allergies' => [], 'medications' => [], 'problems' => [], 'note' => 'invalid_pid']; + } + + $patientService = new PatientService(); + $row = $patientService->findByPid($pid); + $patient = [ + 'pid' => $pid, + 'fname' => $row['fname'] ?? '', + 'lname' => $row['lname'] ?? '', + 'DOB' => $row['DOB'] ?? '', + 'sex' => $row['sex'] ?? '', + ]; + + return [ + 'patient' => $patient, + 'allergies' => $this->fetchListTitles($pid, 'allergy'), + 'medications' => $this->fetchListTitles($pid, 'medication'), + 'problems' => $this->fetchListTitles($pid, 'medical_problem'), + ]; + } + + /** + * @return list + */ + private function fetchListTitles(int $pid, string $type): array + { + $sql = "SELECT title, begdate FROM lists WHERE pid = ? AND type = ? AND enddate IS NULL " + . "ORDER BY date DESC LIMIT " . escape_limit(self::MAX_LIST_ROWS); + $res = sqlStatement($sql, [$pid, $type]); + $out = []; + while ($row = sqlFetchArray($res)) { + $t = trim((string)($row['title'] ?? '')); + if ($t === '') { + continue; + } + $out[] = ['title' => $t, 'begdate' => (string)($row['begdate'] ?? '')]; + } + return $out; + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php new file mode 100644 index 000000000000..0db73b3daa56 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php @@ -0,0 +1,105 @@ +apiKey !== null && $this->apiKey !== ''; + } + + /** + * @param list $messages + * @return array{content:string,usage:array,model:string,raw_status:int} + * @throws \RuntimeException + */ + public function chatJson(string $model, array $messages): array + { + if (!$this->hasApiKey()) { + throw new \RuntimeException('OpenAI API key is not configured'); + } + + $client = new Client(['timeout' => 60]); + try { + $response = $client->post(self::API_URL, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->apiKey, + 'Content-Type' => 'application/json', + ], + 'json' => [ + 'model' => $model, + 'messages' => $messages, + 'response_format' => ['type' => 'json_object'], + 'temperature' => 0.2, + ], + ]); + } catch (GuzzleException $e) { + throw new \RuntimeException('OpenAI request failed: ' . $e->getMessage(), 0, $e); + } + + $status = $response->getStatusCode(); + $body = (string) $response->getBody(); + $decoded = json_decode($body, true); + if (!is_array($decoded)) { + throw new \RuntimeException('OpenAI invalid JSON response'); + } + $content = ''; + if (isset($decoded['choices'][0]['message']['content']) && is_string($decoded['choices'][0]['message']['content'])) { + $content = $decoded['choices'][0]['message']['content']; + } + $usage = []; + if (isset($decoded['usage']) && is_array($decoded['usage'])) { + foreach (['prompt_tokens', 'completion_tokens', 'total_tokens'] as $k) { + if (isset($decoded['usage'][$k])) { + $usage[$k] = (int) $decoded['usage'][$k]; + } + } + } + $modelOut = is_string($decoded['model'] ?? null) ? $decoded['model'] : $model; + + return [ + 'content' => $content, + 'usage' => $usage, + 'model' => $modelOut, + 'raw_status' => $status, + ]; + } + + /** + * Rough USD estimate for observability (not billing); uses public list pricing as defaults. + * + * @param array $usage + */ + public static function estimateCostUsd(string $model, array $usage): float + { + $in = $usage['prompt_tokens'] ?? 0; + $out = $usage['completion_tokens'] ?? 0; + // Defaults for gpt-4o-mini class (adjust in docs if model changes) + $inPer1M = 0.15; + $outPer1M = 0.60; + if (str_contains($model, 'gpt-4o') && !str_contains($model, 'mini')) { + $inPer1M = 2.50; + $outPer1M = 10.00; + } + return round(($in / 1_000_000) * $inPer1M + ($out / 1_000_000) * $outPer1M, 6); + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php new file mode 100644 index 000000000000..67d7117dd5d5 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php @@ -0,0 +1,144 @@ + $toolData Canonical facts the model may cite (dot-path roots). + * @param array{statements?:list}>,uncertainties?:list} $parsed + * @return array{statements:list}>,uncertainties:list,stripped:list} + */ + public function verify(array $toolData, array $parsed): array + { + $outStatements = []; + $uncertainties = $parsed['uncertainties'] ?? []; + if (!is_array($uncertainties)) { + $uncertainties = []; + } + $stripped = []; + $statements = $parsed['statements'] ?? []; + if (!is_array($statements)) { + return ['statements' => [], 'uncertainties' => $this->stringList($uncertainties), 'stripped' => ['(no statements array)']]; + } + + foreach ($statements as $row) { + if (!is_array($row)) { + continue; + } + $text = isset($row['text']) && is_string($row['text']) ? trim($row['text']) : ''; + if ($text === '') { + continue; + } + $citations = $row['citations'] ?? []; + if (!is_array($citations) || $citations === []) { + $stripped[] = $text; + continue; + } + $ok = true; + $cleanCites = []; + foreach ($citations as $c) { + if (!is_string($c) || $c === '') { + $ok = false; + break; + } + if (!$this->citationResolves($toolData, $c)) { + $ok = false; + break; + } + $cleanCites[] = $c; + } + if ($ok) { + $outStatements[] = ['text' => $text, 'citations' => $cleanCites]; + } else { + $stripped[] = $text; + } + } + + return [ + 'statements' => $outStatements, + 'uncertainties' => $this->stringList($uncertainties), + 'stripped' => $stripped, + ]; + } + + /** + * @param list $lines + */ + public function formatForDisplay(array $verified): string + { + $parts = []; + foreach ($verified['statements'] as $s) { + $parts[] = $s['text']; + } + foreach ($verified['uncertainties'] as $u) { + $parts[] = '(' . $u . ')'; + } + if ($verified['stripped'] !== []) { + $parts[] = '[Unverified claims removed: ' . count($verified['stripped']) . ']'; + } + return implode("\n\n", array_filter($parts, fn ($p) => $p !== '')); + } + + /** + * @param mixed $uncertainties + * @return list + */ + private function stringList(mixed $uncertainties): array + { + $out = []; + if (!is_array($uncertainties)) { + return $out; + } + foreach ($uncertainties as $u) { + if (is_string($u) && $u !== '') { + $out[] = $u; + } + } + return $out; + } + + /** + * Dot-path e.g. "patient.fname" or "allergies.0.title" + * + * @param array $root + */ + public function citationResolves(array $root, string $path): bool + { + $segments = explode('.', $path); + $cur = $root; + foreach ($segments as $seg) { + if ($cur === null) { + return false; + } + if (is_array($cur) && array_is_list($cur)) { + if (!ctype_digit($seg)) { + return false; + } + $idx = (int) $seg; + if (!array_key_exists($idx, $cur)) { + return false; + } + $cur = $cur[$idx]; + continue; + } + if (!is_array($cur) || !array_key_exists($seg, $cur)) { + return false; + } + $cur = $cur[$seg]; + } + return $cur !== null && (!is_string($cur) || $cur !== ''); + } +} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig new file mode 100644 index 000000000000..af527e12af3f --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig @@ -0,0 +1,45 @@ +{% extends "patient/card/card_base.html.twig" %} + +{% block content %} +
+

{{ "Inter-visit briefing uses chart data with citations; unverified lines are removed. Demo/synthetic data only."|xlt }}

+ +

+
+ +{% endblock %} diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/version.php b/interface/modules/custom_modules/oe-module-clinical-copilot/version.php new file mode 100644 index 000000000000..9a5f5580559d --- /dev/null +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/version.php @@ -0,0 +1,14 @@ + ['fname' => 'Jane', 'lname' => 'Doe'], + 'allergies' => [['title' => 'Penicillin']], + ]; + $parsed = [ + 'statements' => [ + ['text' => 'First name is Jane.', 'citations' => ['patient.fname']], + ], + 'uncertainties' => [], + ]; + $gate = new VerificationGate(); + $v = $gate->verify($tool, $parsed); + $this->assertCount(1, $v['statements']); + $this->assertSame([], $v['stripped']); + } + + public function testStripsUncitedStatement(): void + { + $tool = ['patient' => ['fname' => 'Jane']]; + $parsed = [ + 'statements' => [ + ['text' => 'Unverifiable claim.', 'citations' => []], + ], + ]; + $gate = new VerificationGate(); + $v = $gate->verify($tool, $parsed); + $this->assertCount(0, $v['statements']); + $this->assertNotEmpty($v['stripped']); + } + + public function testStripsBadCitationPath(): void + { + $tool = ['patient' => ['fname' => 'Jane']]; + $parsed = [ + 'statements' => [ + ['text' => 'Wrong path.', 'citations' => ['patient.not_a_field']], + ], + ]; + $gate = new VerificationGate(); + $v = $gate->verify($tool, $parsed); + $this->assertCount(0, $v['statements']); + } + + public function testListIndexCitation(): void + { + $tool = ['allergies' => [['title' => 'Eggs']]]; + $parsed = [ + 'statements' => [ + ['text' => 'Allergy noted.', 'citations' => ['allergies.0.title']], + ], + ]; + $gate = new VerificationGate(); + $v = $gate->verify($tool, $parsed); + $this->assertCount(1, $v['statements']); + } + + public function testCitationResolvesHelper(): void + { + $gate = new VerificationGate(); + $this->assertTrue($gate->citationResolves(['a' => ['b' => 'x']], 'a.b')); + $this->assertFalse($gate->citationResolves(['a' => ['b' => '']], 'a.b')); + } +} From f7c6128b57bc63dda6377749cf7687f7959f8dd3 Mon Sep 17 00:00:00 2001 From: Monica Peters Date: Thu, 30 Apr 2026 12:57:51 -0400 Subject: [PATCH 04/82] AgentForge PRD1 new module created and manually visually tested. todo: review audit the test coverage relevance accuracy --- .gitignore | 2 + AUDIT.md | 16 +++--- USERS.md | 2 +- .../src/Services/AgentOrchestrator.php | 4 +- .../clinical_copilot/summary_card.html.twig | 2 +- .../Patient/Summary/Card/SectionEvent.php | 7 +-- .../Events/SectionEventAddCardTest.php | 51 +++++++++++++++++++ 7 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 tests/Tests/Isolated/Events/SectionEventAddCardTest.php diff --git a/.gitignore b/.gitignore index 683e9f58fed3..f2ab45f51941 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ ccdaservice/node_modules .buildpath .project .settings +PRD_Week1_AgentForge.jpeg +PRD_Week1_AgentForge.md # testing .phpunit.result.cache diff --git a/AUDIT.md b/AUDIT.md index 4d85d15cb693..9ac6a7cb3138 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -15,7 +15,7 @@ For **HIPAA-aligned design**, the critical dimensions are **where PHI lives** (d **Data quality** is uneven in any real EHR: duplicate problems, stale meds, scanned PDFs versus structured entries, and unsigned notes. **Agent failure modes** mirror these gaps; verification must treat **missing** and **ambiguous** as first-class outcomes. -**Compliance:** Class deployment uses **synthetic chart text** and may call **OpenAI’s API**, with the Week 1 runtime using a **Railway + Docker deployment**. For **production PHI**, this would still require **Business Associate Agreements**, **minimum necessary** payloads, **training / retention** guarantees, subprocessors review, and likely **Azure OpenAI** or equivalent enterprise contracts. **Public observability SaaS** remains **high risk** for PHI unless redacted; for the Railway + Docker deployment, maintain **server-side redacted logs** and explicit retention controls (see [ARCHITECTURE.md](ARCHITECTURE.md)). +**Compliance:** Class deployment uses **synthetic chart text** and may call **OpenAI’s API**, with the Week 1 **public** runtime on **[Cloud Clusters OpenEMR Docker hosting](https://www.cloudclusters.io/cloud/openemr)** (managed SMB-oriented hosting—not Railway). For **production PHI**, this would still require **Business Associate Agreements**, **minimum necessary** payloads, **training / retention** guarantees, subprocessors review, and likely **Azure OpenAI** or equivalent enterprise contracts. **Public observability SaaS** remains **high risk** for PHI unless redacted; for Cloud Clusters, maintain **server-side redacted logs**, understand **hosting-provider** log and backup retention, and keep PHI out of platform telemetry (see [ARCHITECTURE.md](ARCHITECTURE.md)). **Bottom line:** OpenEMR is a **credible foundation** for a **session-bound, in-process** Clinical Co-Pilot that **reuses existing authorization layers** and limits LLM exposure to **necessary structured excerpts**. The highest-impact audit outcome is **not** “avoid OpenEMR,” but **control integration surface area**: strict tool contracts, mandatory verification, **no PHI in third-party traces**, and a documented **upgrade path** to OAuth2-scoped APIs before real clinical deployment. @@ -54,7 +54,7 @@ For **HIPAA-aligned design**, the critical dimensions are **where PHI lives** (d - **PHP error logs** (stack traces can include SQL or paths). - **LLM vendor logs** (policy-dependent; assume sensitive). - **Observability third parties** (default: **do not send** raw prompts/responses). -- **Platform logs on Railway** (treat as sensitive operational telemetry; keep PHI out of log lines). +- **Platform / hosting logs (Cloud Clusters)** (treat as sensitive operational telemetry; keep PHI out of log lines). ### PHI handling gaps (inherent to integration, not solely OpenEMR) @@ -142,13 +142,13 @@ flowchart LR - If logs contain **PHI**, they become **high-sensitivity assets** with retention limits. - Sprint approach: **synthetic PHI only**; logs **redacted**. -- In the Railway + Docker deployment, document the ownership split: app-level redaction is your responsibility; platform log access, retention windows, and export paths must be explicitly reviewed and configured. +- On **Cloud Clusters** managed OpenEMR, document the ownership split: app-level redaction is your responsibility; **provider** control panel, backups, WAF, and support access paths must be explicitly reviewed for anything beyond synthetic demo data. -### Railway operational controls (Week 1 hosting model) +### Cloud Clusters operational controls (Week 1 hosting model) -- Keep all secrets in Railway environment/service variables; never commit secrets or bake them into container images. -- Pin deployment region deliberately and verify data-flow implications for external API egress. -- Define backup/restore accountability for DB and persisted application storage before any non-demo usage. +- Keep all secrets (DB, OpenEMR admin, **OpenAI API keys**) in the **provider control panel** and OpenEMR secured configuration—never commit secrets or bake them into custom images you push to git. +- Pin **data center / region** deliberately and verify data-flow implications for OpenAI API egress and subprocessors. +- Use vendor **backup / restore** features per plan; define who can download backups and where those files may **not** be stored (e.g. unencrypted consumer cloud) before any non-demo usage. ### BAA assumption (per PRD) @@ -172,7 +172,7 @@ Gauntlet Week 1 PRD: act **as if** a **Business Associate Agreement** (or equiva | Artifact | Path | |----------|------| -| PRD | `Documentation/PRD_Week1_AgentForge.pdf` | +| PRD | `PRD_Week1_AgentForge.md` | | Architecture decisions | [ARCHITECTURE.md](ARCHITECTURE.md) | | Personas / use cases | [USERS.md](USERS.md) | | Front controller | `public/index.php` | diff --git a/USERS.md b/USERS.md index 86e8e4ddf031..436e2b9ad1d3 100644 --- a/USERS.md +++ b/USERS.md @@ -12,7 +12,7 @@ This document is the **source of truth** for *who* the Clinical Co-Pilot serves **Setting:** Community **outpatient clinic** with a **high-volume schedule** (approximately fifteen to twenty-five face-to-face visits per day), mixed acute and chronic care, plus inbox and results tasks between visits. -**Technical context:** The physician already uses **OpenEMR** for scheduling, charting, e-prescribing, and lab/imaging review; for this Week 1 project, that OpenEMR environment is delivered via a **Railway + Docker deployment**. They move between exam rooms with **roughly one minute or less** between patients to re-orient on the next chart. +**Technical context:** The physician already uses **OpenEMR** for scheduling, charting, e-prescribing, and lab/imaging review; for this Week 1 project, the **public demo** OpenEMR instance is hosted on **[Cloud Clusters](https://www.cloudclusters.io/cloud/openemr)** managed **OpenEMR Docker** (a pattern many SMB practices already use and trust), while day-to-day product thinking still assumes a normal clinic deployment of OpenEMR. They move between exam rooms with **roughly one minute or less** between patients to re-orient on the next chart. **Goals:** Minimize cognitive load, avoid missing important changes since the last visit, and enter the room with an accurate mental model without reading the entire chart. diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php index c0f41c4ca6b0..88af49211660 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php @@ -65,12 +65,12 @@ public function runBriefing(int $sessionPid, AgentTelemetry $telemetry, string $ - Every factual clinical statement MUST include one or more citations; each citation must be a valid path into CHART_JSON (e.g. patient.fname, allergies.0.title). - If data is missing, put a short note in uncertainties; do not invent facts. - Do not give dosing or new diagnoses. -- Keep the briefing brief (under 120 words across statements). +- Keep the briefing concise andbrief (under 120 words across statements). PROMPT; $messages = [ ['role' => 'system', 'content' => $system], - ['role' => 'user', 'content' => "CHART_JSON:\n" . $toolJson . "\n\nTask: one-paragraph room briefing: who this is, why they may be here if inferable from problems only as possibilities labeled uncertain, allergies, key meds. Use citations."], + ['role' => 'user', 'content' => "CHART_JSON:\n" . $toolJson . "\n\nTask: one-paragraph room briefing: why this patient may be here if inferable from problems only as possibilities labeled uncertain, allergies, key medications. Use citations."], ]; try { diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig index af527e12af3f..3bb561095e63 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig @@ -2,7 +2,7 @@ {% block content %}
-

{{ "Inter-visit briefing uses chart data with citations; unverified lines are removed. Demo/synthetic data only."|xlt }}

+

{{ "Demo / Synthetic Data: Inter-visit briefing uses chart data with citations; unverified lines are removed."|xlt }}


 
diff --git a/src/Events/Patient/Summary/Card/SectionEvent.php b/src/Events/Patient/Summary/Card/SectionEvent.php index 52b85b493d1a..3a401791de30 100644 --- a/src/Events/Patient/Summary/Card/SectionEvent.php +++ b/src/Events/Patient/Summary/Card/SectionEvent.php @@ -73,11 +73,8 @@ public function addCard(CardInterface $card, $position = null): void throw new DomainException("Card {$card->getIdentifier()} is not unique in current list"); } - // @todo ensure position is an integer or null - // if (!is_int($position) || !is_null($position)) { - // throw new LogicException('Position parameter must be either null or an interger'); - // } - if ($position == null || !is_int($position)) { + // Use === null so 0 (prepend) is not confused with null (append): 0 == null is true in PHP. + if ($position === null || !is_int($position)) { $this->cards[] = $card; } else { array_splice($this->cards, $position, 0, [$card]); diff --git a/tests/Tests/Isolated/Events/SectionEventAddCardTest.php b/tests/Tests/Isolated/Events/SectionEventAddCardTest.php new file mode 100644 index 000000000000..41245e1cc73d --- /dev/null +++ b/tests/Tests/Isolated/Events/SectionEventAddCardTest.php @@ -0,0 +1,51 @@ +createCardMock('first'); + $second = $this->createCardMock('second'); + $event->addCard($first); + $event->addCard($second, 0); + + $ids = array_map(static fn (CardInterface $c) => $c->getIdentifier(), $event->getCards()); + $this->assertSame(['second', 'first'], $ids); + } + + public function testNullPositionAppends(): void + { + $event = new SectionEvent('primary'); + $a = $this->createCardMock('a'); + $b = $this->createCardMock('b'); + $event->addCard($a); + $event->addCard($b, null); + + $ids = array_map(static fn (CardInterface $c) => $c->getIdentifier(), $event->getCards()); + $this->assertSame(['a', 'b'], $ids); + } + + private function createCardMock(string $identifier): CardInterface + { + $m = $this->createMock(CardInterface::class); + $m->method('getIdentifier')->willReturn($identifier); + + return $m; + } +} From b39c7a817b6d97dbb7ce6d5a441837f4dadd7a05 Mon Sep 17 00:00:00 2001 From: Monica Peters Date: Thu, 30 Apr 2026 17:01:03 -0400 Subject: [PATCH 05/82] updated Clinical Co-Pilot Module with polished code headers, sanitized openai responses, formatted token usage on the visual card --- .../oe-module-clinical-copilot/README.md | 15 ++++++++ .../oe-module-clinical-copilot/info.txt | 5 ++- .../moduleConfig.php | 29 ++++++++++++--- .../openemr.bootstrap.php | 27 ++++++++++++-- .../public/copilot_request.php | 30 ++++++++++++++-- .../src/Bootstrap.php | 25 +++++++++++-- .../src/ClinicalCopilotCard.php | 25 +++++++++++-- .../Controller/CopilotRequestController.php | 29 +++++++++++++-- .../src/Services/AgentOrchestrator.php | 28 +++++++++++++-- .../src/Services/AgentTelemetry.php | 26 ++++++++++++-- .../src/Services/ChartContextTool.php | 36 +++++++++++++++---- .../src/Services/OpenAiClient.php | 26 ++++++++++++-- .../src/Services/VerificationGate.php | 26 ++++++++++++-- .../clinical_copilot/summary_card.html.twig | 22 ++++++++++-- .../oe-module-clinical-copilot/version.php | 27 ++++++++++++-- 15 files changed, 332 insertions(+), 44 deletions(-) diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/README.md b/interface/modules/custom_modules/oe-module-clinical-copilot/README.md index 5d0ca476838f..910d96190080 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/README.md +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/README.md @@ -1,3 +1,18 @@ + # Clinical Co-Pilot (`oe-module-clinical-copilot`) AgentForge Week 1 module: **inter-visit briefing** on the **patient summary** (primary dashboard column) using OpenAI, **citation-backed** statements, **structured telemetry**, and **PHPUnit-isolated** verification tests. diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/info.txt b/interface/modules/custom_modules/oe-module-clinical-copilot/info.txt index 6d1c8a81ad0f..7d49a5aade51 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/info.txt +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/info.txt @@ -1 +1,4 @@ -Clinical Co-Pilot (AgentForge) v0.1.0 — conversational briefing on patient summary with verification, telemetry, and OpenAI. +Clinical Co-Pilot (AgentForge) v0.1.0 — Patient summary with verification, telemetry, and OpenAI. +Author Monica Peters GauntletAI.com | SPDX-License-Identifier: GPL-3.0-only +Module path: interface/modules/custom_modules/oe-module-clinical-copilot | Version 0.1.0 | 2026-04-30 +Line 1 is the OpenEMR module registry display name (InstModuleTable uses info.txt line 0 only). diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php b/interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php index c98161dc22f3..b2e76dd5e5c1 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/moduleConfig.php @@ -1,17 +1,38 @@ + * + * @author Monica Peters GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Edit `globals`, `require`, and descriptive fields here; keep `version` in sync with `version.php`. + * + * Usage example (integrator): + * Merge this folder under `interface/modules/custom_modules/oe-module-clinical-copilot/`, then + * register/enable the module in Admin → System → Modules — OpenEMR reads this file at install time. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md version.php openemr.bootstrap.php */ return [ 'name' => 'Clinical Co-Pilot (AgentForge)', 'description' => 'AI-assisted inter-visit briefing on the patient dashboard with citation-backed output, eval hooks, and structured telemetry.', 'version' => '0.1.0', - 'author' => 'AgentForge fork', + 'author' => 'Monica Peters GauntletAI.com', 'license' => 'GPL-3.0', 'acl_category' => 'patients', 'acl_section' => 'demo', diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php b/interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php index 425d5dd1301a..f108d8a710ac 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php @@ -1,10 +1,31 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * OpenEMR loads this file when the module is enabled; do not rename. Autoload maps `src/` to + * `OpenEMR\Modules\ClinicalCopilot\`. + * + * Usage example (integrator): + * Ensure this file exists at `interface/modules/custom_modules/oe-module-clinical-copilot/openemr.bootstrap.php` + * alongside `src/` and `moduleConfig.php`, then enable the module in Admin → System → Modules. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md moduleConfig.php src/Bootstrap.php */ namespace OpenEMR\Modules\ClinicalCopilot; diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php b/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php index 03f86b186592..0ae75741b498 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/public/copilot_request.php @@ -1,10 +1,34 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Invoked from the patient-summary Twig card via same-origin `fetch` POST with CSRF token; + * requires an authenticated session and active patient context. + * + * Usage example (integrator): + * POST to this script’s URL with `csrf_token_form` (same pattern as core OpenEMR forms); do not pass + * patient id in the body for authorization — the controller binds to session `pid` only. + * + * Security: Trust boundary uses the active OpenEMR session `pid` only; never honor a client-supplied + * patient identifier for chart or model context. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md src/Controller/CopilotRequestController.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php index b90d4b8115ad..77fe32511544 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Bootstrap.php @@ -1,10 +1,29 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Instantiated from `openemr.bootstrap.php`; subscribe listeners once per request when the module loads. + * + * Usage example (integrator): + * Fork this class only if you need additional OpenEMR events; keep `subscribeToEvents()` idempotent patterns. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md openemr.bootstrap.php ClinicalCopilotCard.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php index 99e0310b021d..bfca23ebde24 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/ClinicalCopilotCard.php @@ -1,10 +1,29 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Registered via `Bootstrap` on the patient summary section event; renders `summary_card.html.twig`. + * + * Usage example (integrator): + * Change `TEMPLATE` or card options here to alter dashboard placement/labels; keep ACL aligned with `moduleConfig.php`. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md templates/clinical_copilot/summary_card.html.twig public/copilot_request.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php index 3efca61eeda7..6a4d9cca5e60 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Controller/CopilotRequestController.php @@ -1,10 +1,33 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Called only from `public/copilot_request.php` after `globals.php` bootstrap; returns JSON for the UI card. + * + * Usage example (integrator): + * Reuse patterns here (CSRF + session pid) if you add sibling endpoints; do not expose chart data without ACL checks. + * + * Security: Trust boundary uses the active OpenEMR session `pid` only; never honor a client-supplied + * patient identifier for briefing or tool JSON. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @subpackage Controller + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md public/copilot_request.php Services/AgentOrchestrator.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php index 88af49211660..a4324a95eac1 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentOrchestrator.php @@ -1,10 +1,32 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Construct with `ChartContextTool`, `OpenAiClient`, and `VerificationGate`; call `runBriefing()` with session pid. + * + * Usage example (integrator): + * Inject alternate clients here for tests; keep telemetry and verification hooks for observability contracts. + * + * Security: Callers must pass the session-validated patient id only; this class assumes upstream authZ/authN. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @subpackage Services + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md ChartContextTool.php OpenAiClient.php VerificationGate.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php index 94c15050af2f..5ca8bfb205d7 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/AgentTelemetry.php @@ -1,10 +1,30 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Instantiate per request; `mark()` steps then `flush()` to logging sink with request/correlation context. + * + * Usage example (integrator): + * Wrap new pipeline stages with `mark('stage_name', true/false, 'optional detail')` for consistent JSON logs. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @subpackage Services + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md AgentOrchestrator.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php index 549ebe308621..5645e4ba19ee 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/ChartContextTool.php @@ -1,10 +1,32 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Call `collectForPatient($pid)` only after the controller confirms ACL and session binding for that pid. + * + * Usage example (integrator): + * Extend with additional bounded queries; keep row caps and PHI minimization consistent with site policy. + * + * Security: Pass validated session `pid` only; never use unvalidated client input as `$pid`. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @subpackage Services + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md AgentOrchestrator.php VerificationGate.php */ declare(strict_types=1); @@ -33,9 +55,11 @@ public function collectForPatient(int $pid): array $row = $patientService->findByPid($pid); $patient = [ 'pid' => $pid, - 'fname' => $row['fname'] ?? '', - 'lname' => $row['lname'] ?? '', - 'DOB' => $row['DOB'] ?? '', + # Neutralize for HIIPAA Compliance + # Remove completely or Cryptographic Hash and Salt + # 'fname' => $row['fname'] ?? '', + # 'lname' => $row['lname'] ?? '', + # 'DOB' => $row['DOB'] ?? '', 'sex' => $row['sex'] ?? '', ]; diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php index 0db73b3daa56..4d1c1bc9a83c 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/OpenAiClient.php @@ -1,10 +1,30 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Construct with API key from environment/Globals; call `chatJson()` with model id and message list. + * + * Usage example (integrator): + * Set `CLINICAL_COPILOT_OPENAI_API_KEY` or `OPENAI_API_KEY` in the web runtime; never embed keys in source. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @subpackage Services + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md moduleConfig.php AgentOrchestrator.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php index 67d7117dd5d5..2c53c2fcbad9 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/src/Services/VerificationGate.php @@ -1,10 +1,30 @@ GauntletAI.com + * @version 0.1.0 + * @since 2026-04-30 + * + * Usage: + * Pure PHP: invoke `verify($toolData, $parsed)` in unit tests without full OpenEMR bootstrap. + * + * Usage example (integrator): + * Feed the same JSON structure the model was prompted to cite; assert stripped lines in PHPUnit fixtures. + * + * @package OpenEMR\Modules\ClinicalCopilot + * @subpackage Services + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + * @link https://www.open-emr.org/wiki/index.php/Developers#Custom_Modules + * @see README.md AgentOrchestrator.php */ declare(strict_types=1); diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig index 3bb561095e63..27843923f445 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig @@ -1,9 +1,25 @@ {% extends "patient/card/card_base.html.twig" %} +{# + SPDX-License-Identifier: GPL-3.0-only + + summary_card.html.twig — Patient summary card UI for Clinical Co-Pilot (briefing button + output). + + Module: oe-module-clinical-copilot (OpenEMR\Modules\ClinicalCopilot) + Author: Monica Peters GauntletAI.com + Version: 0.1.0 | @since 2026-04-30 + + Usage: Rendered via ClinicalCopilotCard; posts to copilotAjaxUrl with CSRF (see public/copilot_request.php). + + Usage example (integrator): Override this template path in Bootstrap/Twig loader only if you fork the module; + keep data-pid/data-ajax-url/data-csrf attributes for the bundled script. + + @see README.md ClinicalCopilotCard.php +#} {% block content %}
-

{{ "Demo / Synthetic Data: Inter-visit briefing uses chart data with citations; unverified lines are removed."|xlt }}

- +

{{ "Quick clinical overview for this visit. (DEMO)"|xlt }}

+

 
From 3db65097dc4f4224477b9657147ec306149b310a Mon Sep 17 00:00:00 2001 From: Monica Peters Date: Fri, 1 May 2026 18:15:18 -0400 Subject: [PATCH 07/82] Add from-source Docker build for Railway --- docker/from-source/Dockerfile | 45 +++++++++++++++++++ docker/from-source/README.md | 44 ++++++++++++++++++ docker/from-source/docker-compose.yml | 65 +++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 docker/from-source/Dockerfile create mode 100644 docker/from-source/README.md create mode 100644 docker/from-source/docker-compose.yml diff --git a/docker/from-source/Dockerfile b/docker/from-source/Dockerfile new file mode 100644 index 000000000000..e833697e1096 --- /dev/null +++ b/docker/from-source/Dockerfile @@ -0,0 +1,45 @@ +# OpenEMR built from this repository (build context must be repo root). +# See docker/from-source/README.md +# +# hadolint ignore=DL3008,DL3015 +FROM openemr/openemr:flex + +# Flex is Alpine-based; root required for package installs (matches flex runtime expectations). +# hadolint ignore=DL3002 +USER root + +# Node 24 + build toolchain for npm/gulp and ccdaservice native deps (e.g. libxmljs2). +# hadolint ignore=DL3018 +RUN apk update \ + && apk add --no-cache \ + g++ \ + git \ + libxml2-dev \ + linux-headers \ + make \ + nodejs \ + npm \ + python3 \ + && rm -rf /var/cache/apk/* + +WORKDIR /var/www/localhost/htdocs/openemr + +COPY . . + +ENV COMPOSER_ALLOW_SUPERUSER=1 + +RUN composer install --no-dev --no-interaction --prefer-dist \ + && npm ci \ + && npm run build \ + && (cd ccdaservice && npm ci) \ + && composer dump-autoload -o \ + && rm -rf /root/.composer/cache /tmp/* /var/tmp/* + +ARG SOURCE_BRANCH=master +ARG GIT_COMMIT=unknown +LABEL org.opencontainers.image.title="OpenEMR (from source)" \ + org.opencontainers.image.source="https://github.com/openemr/openemr" \ + org.opencontainers.image.version="${SOURCE_BRANCH}" \ + org.opencontainers.image.revision="${GIT_COMMIT}" + +EXPOSE 80 443 diff --git a/docker/from-source/README.md b/docker/from-source/README.md new file mode 100644 index 000000000000..de71902aa552 --- /dev/null +++ b/docker/from-source/README.md @@ -0,0 +1,44 @@ +# OpenEMR from this repository (Docker) + +Build and run OpenEMR from **your local clone** (for example `master`), not the pre-built `openemr/openemr:latest` image on Docker Hub. + +The image extends [`openemr/openemr:flex`](https://hub.docker.com/r/openemr/openemr/) (Alpine-based) so PHP extensions, Apache, and container entrypoints match the usual OpenEMR Docker workflow. Node.js and build tools are added with `apk` during the image build. + +## Build only (context = repository root) + +```shell +docker build -f docker/from-source/Dockerfile -t openemr:from-source . +``` + +Optional labels: + +```shell +docker build -f docker/from-source/Dockerfile -t openemr:from-source \ + --build-arg GIT_COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)" \ + --build-arg SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo master)" \ + . +``` + +## Run with MariaDB (same pattern as production compose) + +From the **repository root**: + +```shell +docker compose -f docker/from-source/docker-compose.yml up +``` + +Or from this directory: + +```shell +docker compose -f docker-compose.yml up +``` + +First startup can take several minutes (database + OpenEMR setup). Default demo credentials match `docker/production/docker-compose.yml` (`admin` / `pass` unless you change `OE_USER` / `OE_PASS`). **Change all passwords and secrets before any real deployment.** + +## Volumes + +`docker-compose.yml` defines named volumes for MySQL data, OpenEMR `sites`, and logs—same idea as [docker/production/docker-compose.yml](../production/docker-compose.yml). + +## More Docker documentation + +See [DOCKER_README.md](../../DOCKER_README.md) for production vs development images on Docker Hub. diff --git a/docker/from-source/docker-compose.yml b/docker/from-source/docker-compose.yml new file mode 100644 index 000000000000..7dba5f01acf4 --- /dev/null +++ b/docker/from-source/docker-compose.yml @@ -0,0 +1,65 @@ +# OpenEMR image built from this repository + MariaDB (same layout as docker/production). +# MYSQL_HOST and MYSQL_ROOT_PASS are required for openemr +# MYSQL_USER, MYSQL_PASS, OE_USER, OE_PASS are optional for openemr +# if not provided, then default to openemr, openemr, admin, and pass respectively. +services: + mysql: + restart: always + image: mariadb:11.8.6@sha256:78a5047d3ba33975f183f183c2464cc7f1eab13ec8667e57cc9a5821d6da7577 + command: ['mariadbd', '--character-set-server=utf8mb4'] + volumes: + - databasevolume:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: root + healthcheck: + test: + - CMD + - /usr/local/bin/healthcheck.sh + - --su-mysql + - --connect + - --innodb_initialized + start_period: 1m + start_interval: 10s + interval: 1m + timeout: 5s + retries: 3 + openemr: + restart: always + build: + context: ../.. + dockerfile: docker/from-source/Dockerfile + ports: + - 80:80 + - 443:443 + volumes: + - logvolume01:/var/log + - sitevolume:/var/www/localhost/htdocs/openemr/sites + environment: + MYSQL_HOST: mysql + MYSQL_ROOT_PASS: root + MYSQL_USER: openemr + MYSQL_PASS: openemr + OE_USER: admin + OE_PASS: pass + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: + - CMD + - /usr/bin/curl + - --fail + - --insecure + - --location + - --show-error + - --silent + - https://localhost/meta/health/readyz + start_period: 3m + start_interval: 10s + interval: 1m + timeout: 5s + retries: 3 +volumes: + logvolume01: {} + sitevolume: {} + databasevolume: {} From 4035ef2cd216f6c1ece85ac5bdb8f46070f8a0ae Mon Sep 17 00:00:00 2001 From: Monica Peters Date: Fri, 1 May 2026 18:18:57 -0400 Subject: [PATCH 08/82] Add tests and Clinical Co-Pilot minor updates --- .../clinical_copilot/summary_card.html.twig | 2 +- .../CitationLabelBuilderTest.php | 29 ++++++++ .../ClinicalDomainRulesTest.php | 68 +++++++++++++++++++ .../OpenAiClientMergeUsageIsolatedTest.php | 26 +++++++ .../ToolRegistryIsolatedTest.php | 46 +++++++++++++ .../VerificationGateIsolatedTest.php | 41 +++++++++++ 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/Isolated/ClinicalCopilot/CitationLabelBuilderTest.php create mode 100644 tests/Tests/Isolated/ClinicalCopilot/ClinicalDomainRulesTest.php create mode 100644 tests/Tests/Isolated/ClinicalCopilot/OpenAiClientMergeUsageIsolatedTest.php create mode 100644 tests/Tests/Isolated/ClinicalCopilot/ToolRegistryIsolatedTest.php diff --git a/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig index 1836f39913ac..1cf711921859 100644 --- a/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig +++ b/interface/modules/custom_modules/oe-module-clinical-copilot/templates/clinical_copilot/summary_card.html.twig @@ -10,7 +10,7 @@

{{ "Inter-visit co-pilot: briefing and follow-up questions. Citations reference chart tools only. (DEMO)"|xlt }}

diff --git a/tests/Tests/Isolated/ClinicalCopilot/CitationLabelBuilderTest.php b/tests/Tests/Isolated/ClinicalCopilot/CitationLabelBuilderTest.php new file mode 100644 index 000000000000..f9fd55053089 --- /dev/null +++ b/tests/Tests/Isolated/ClinicalCopilot/CitationLabelBuilderTest.php @@ -0,0 +1,29 @@ + ['allergies' => [['title' => 'SECRET_VALUE']]], + 'recent_encounters' => ['encounters' => [['date' => '2020-01-01', 'reason' => 'X']]], + ]; + $labels = $b->labelsForPaths($merged, ['chart_lists.allergies.0.title', 'recent_encounters.encounters.0.reason']); + $this->assertSame('Allergy list', $labels[0]['label']); + $this->assertStringContainsString('Recent encounter', $labels[1]['label']); + $this->assertStringNotContainsString('SECRET_VALUE', $labels[0]['label']); + } +} diff --git a/tests/Tests/Isolated/ClinicalCopilot/ClinicalDomainRulesTest.php b/tests/Tests/Isolated/ClinicalCopilot/ClinicalDomainRulesTest.php new file mode 100644 index 000000000000..53e0eb8e19ba --- /dev/null +++ b/tests/Tests/Isolated/ClinicalCopilot/ClinicalDomainRulesTest.php @@ -0,0 +1,68 @@ + [ + ['text' => 'Take 500 mg daily for pain.', 'citations' => ['chart_lists.medications.0.title']], + ], + 'uncertainties' => [], + 'stripped' => [], + ]; + $merged = ['chart_lists' => ['medications' => [['title' => 'Aspirin']]]]; + $out = $rules->apply($verified, $merged); + $this->assertCount(0, $out['statements']); + $this->assertNotEmpty($out['domain_stripped']); + } + + public function testStripsDefinitiveNewDiagnosisPhrasing(): void + { + $rules = new ClinicalDomainRules(); + $verified = [ + 'statements' => [ + ['text' => 'You have diabetes per chart.', 'citations' => ['chart_lists.problems.0.title']], + ], + 'uncertainties' => [], + 'stripped' => [], + ]; + $merged = ['chart_lists' => ['problems' => [['title' => 'Diabetes']]]]; + $out = $rules->apply($verified, $merged); + $this->assertCount(0, $out['statements']); + } + + public function testPolypharmacyAddsUncertainty(): void + { + $rules = new ClinicalDomainRules(); + $meds = []; + for ($i = 0; $i < 15; $i++) { + $meds[] = ['title' => 'Med' . $i]; + } + $verified = [ + 'statements' => [ + ['text' => 'Review medication list.', 'citations' => ['chart_lists.medications.0.title']], + ], + 'uncertainties' => [], + 'stripped' => [], + ]; + $merged = ['chart_lists' => ['medications' => $meds]]; + $out = $rules->apply($verified, $merged); + $this->assertTrue( + count(array_filter($out['uncertainties'], static fn ($u) => str_contains((string) $u, 'Polypharmacy'))) > 0 + ); + } +} diff --git a/tests/Tests/Isolated/ClinicalCopilot/OpenAiClientMergeUsageIsolatedTest.php b/tests/Tests/Isolated/ClinicalCopilot/OpenAiClientMergeUsageIsolatedTest.php new file mode 100644 index 000000000000..382be8f215a4 --- /dev/null +++ b/tests/Tests/Isolated/ClinicalCopilot/OpenAiClientMergeUsageIsolatedTest.php @@ -0,0 +1,26 @@ + 10, 'completion_tokens' => 5, 'total_tokens' => 15]; + $b = ['prompt_tokens' => 3, 'completion_tokens' => 2, 'total_tokens' => 5]; + $m = OpenAiClient::mergeUsageTokens($a, $b); + $this->assertSame(13, $m['prompt_tokens']); + $this->assertSame(7, $m['completion_tokens']); + $this->assertSame(20, $m['total_tokens']); + } +} diff --git a/tests/Tests/Isolated/ClinicalCopilot/ToolRegistryIsolatedTest.php b/tests/Tests/Isolated/ClinicalCopilot/ToolRegistryIsolatedTest.php new file mode 100644 index 000000000000..3b8c986022fd --- /dev/null +++ b/tests/Tests/Isolated/ClinicalCopilot/ToolRegistryIsolatedTest.php @@ -0,0 +1,46 @@ +collectMerged(0); + $this->assertArrayHasKey('chart_lists', $merged); + $this->assertArrayHasKey('recent_encounters', $merged); + $this->assertArrayHasKey('recent_labs', $merged); + $this->assertSame('invalid_pid', $merged['chart_lists']['note'] ?? null); + } + + public function testOpenAiToolsArrayNonEmpty(): void + { + $registry = new ToolRegistry( + new ChartListsTool(new ChartContextTool()), + new RecentEncountersTool(), + new RecentLabsTool(), + ); + $tools = $registry->openAiToolsArray(); + $this->assertCount(3, $tools); + $this->assertSame('function', $tools[0]['type'] ?? null); + } +} diff --git a/tests/Tests/Isolated/ClinicalCopilot/VerificationGateIsolatedTest.php b/tests/Tests/Isolated/ClinicalCopilot/VerificationGateIsolatedTest.php index 539fb4528a19..27f711eae718 100644 --- a/tests/Tests/Isolated/ClinicalCopilot/VerificationGateIsolatedTest.php +++ b/tests/Tests/Isolated/ClinicalCopilot/VerificationGateIsolatedTest.php @@ -80,4 +80,45 @@ public function testCitationResolvesHelper(): void $this->assertTrue($gate->citationResolves(['a' => ['b' => 'x']], 'a.b')); $this->assertFalse($gate->citationResolves(['a' => ['b' => '']], 'a.b')); } + + public function testKeepsStatementWithMultipleCitationsAllValid(): void + { + $tool = [ + 'chart_lists' => [ + 'allergies' => [['title' => 'Penicillin']], + 'medications' => [['title' => 'Lisinopril']], + ], + ]; + $parsed = [ + 'statements' => [ + [ + 'text' => 'Allergy and med noted.', + 'citations' => ['chart_lists.allergies.0.title', 'chart_lists.medications.0.title'], + ], + ], + 'uncertainties' => [], + ]; + $gate = new VerificationGate(); + $v = $gate->verify($tool, $parsed); + $this->assertCount(1, $v['statements']); + $this->assertCount(2, $v['statements'][0]['citations']); + } + + public function testMergedBundleRecentEncountersPath(): void + { + $tool = [ + 'recent_encounters' => [ + 'encounters' => [['date' => '2024-01-02', 'reason' => 'Follow-up', 'visit_category' => 'Office']], + ], + ]; + $parsed = [ + 'statements' => [ + ['text' => 'Recent visit documented.', 'citations' => ['recent_encounters.encounters.0.reason']], + ], + 'uncertainties' => [], + ]; + $gate = new VerificationGate(); + $v = $gate->verify($tool, $parsed); + $this->assertCount(1, $v['statements']); + } } From 3095a0c4640c7e1e56205e2fa4679c3e2cee3359 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 27 Apr 2026 15:02:49 -0700 Subject: [PATCH 09/82] fix(db): Fix SQL upgrade syntax (#11866) #### Short description of what this changes or resolves: While working on #11805 I came across a bug when running a SQL upgrade. Invalid syntax! Since the block was conditional, you wouldn't always hit it. The Inferno data did. #### Changes proposed in this pull request: Splits the index additions into a syntactically-legal version without changing the results. Well, technically the new conditionals could change the results, but the intent of what it did before was clear. #### Was an AI assistant used? Yes / No No --- sql/7_0_3-to-7_0_4_upgrade.sql | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sql/7_0_3-to-7_0_4_upgrade.sql b/sql/7_0_3-to-7_0_4_upgrade.sql index e583607b6b21..e7eb19b3177e 100644 --- a/sql/7_0_3-to-7_0_4_upgrade.sql +++ b/sql/7_0_3-to-7_0_4_upgrade.sql @@ -1309,10 +1309,15 @@ ALTER TABLE `procedure_order` COMMENT 'FHIR intent: order, plan, directive, proposal', ADD COLUMN `location_id` INT DEFAULT NULL COMMENT 'References facility.id for service location (FHIR locationReference)'; -ALTER TABLE `procedure_order` - ADD INDEX IF NOT EXISTS `idx_scheduled_date` (`scheduled_date`), - ADD INDEX IF NOT EXISTS `idx_order_intent` (`order_intent`), - ADD INDEX IF NOT EXISTS `idx_location_id` (`location_id`); +#EndIf +#IfNotIndex procedure_order idx_scheduled_date +ALTER TABLE `procedure_order` ADD INDEX `idx_scheduled_date` (`scheduled_date`); +#EndIf +#IfNotIndex procedure_order idx_order_intent +ALTER TABLE `procedure_order` ADD INDEX `idx_order_intent` (`order_intent`), +#EndIf +#IfNotIndex procedure_order idx_location_id +ALTER TABLE `procedure_order` ADD INDEX `idx_location_id` (`location_id`); #EndIf #IfNotRow2D list_options list_id ord_priority option_id routine From e883b9ee7b1e1d001337ba822244ebc21c34fb3f Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 27 Apr 2026 15:06:30 -0700 Subject: [PATCH 10/82] chore(db): Fix typo (#11867) Correction from #11866, which was incorrectly extracted from #11805. --- sql/7_0_3-to-7_0_4_upgrade.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/7_0_3-to-7_0_4_upgrade.sql b/sql/7_0_3-to-7_0_4_upgrade.sql index e7eb19b3177e..ed2112b8f459 100644 --- a/sql/7_0_3-to-7_0_4_upgrade.sql +++ b/sql/7_0_3-to-7_0_4_upgrade.sql @@ -1314,7 +1314,7 @@ ALTER TABLE `procedure_order` ALTER TABLE `procedure_order` ADD INDEX `idx_scheduled_date` (`scheduled_date`); #EndIf #IfNotIndex procedure_order idx_order_intent -ALTER TABLE `procedure_order` ADD INDEX `idx_order_intent` (`order_intent`), +ALTER TABLE `procedure_order` ADD INDEX `idx_order_intent` (`order_intent`); #EndIf #IfNotIndex procedure_order idx_location_id ALTER TABLE `procedure_order` ADD INDEX `idx_location_id` (`location_id`); From cfb1f12e008f84711496456f82c213f0e29203c8 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Mon, 27 Apr 2026 19:58:17 -0400 Subject: [PATCH 11/82] ci(e2e): capture diagnostics for InvalidSessionIdException flake (#11857) Refs #11725. ## Summary Diagnostics-only change motivated by the research in #11725. The Selenium standalone-chromium container occasionally returns `invalid session id: session deleted as the browser has closed the connection` mid-test, but the existing post-test `dc logs selenium` step has shown no OOM kill, no tab-crashed event, and no supervisord restart at the moment of failure. We can't pin down the silent Chrome death without more evidence, so this PR makes the next failure leave a usable trace. Adds a failure-only step in the e2e job in `.github/workflows/test.yml` that writes the following to a `selenium-diagnostics/` directory and uploads it as a separate artifact: - `dmesg | tail -200` from the runner (OOM-killer / segfault lines) - `docker compose top selenium` (process tree inside the container at teardown) - `docker stats --no-stream selenium` (memory/CPU at teardown) - Full `docker compose logs selenium` written to a file (also still echoed to the job log by the existing step, but the file is downloadable alongside the rest) No test behavior change. No image bumps, no `shm_size`, no restart policies. The new steps are gated on `failure() && matrix.suite == 'e2e'` so green builds are unaffected. ## Test plan - [ ] Merge and wait for the next e2e flake on master - [ ] On failure, download the `selenium-diagnostics-` artifact from the run - [ ] Inspect `dmesg.tail.txt`, `docker-compose-top.txt`, `docker-stats.txt`, and `selenium.log` for OOM-killer / segfault / resource-pressure signals around the failure timestamp - [ ] Feed findings back into #11725 to choose the actual fix --- .github/workflows/test.yml | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b50512cab8d0..081a17170a6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -619,6 +619,46 @@ jobs: name: junit-${{ needs.setup.outputs.docker_dir }}-${{ matrix.suite }} path: junit-${{ matrix.suite }}.xml + ## + # Selenium diagnostics for InvalidSessionIdException flake (#11725). + # Runs FIRST on failure so the selenium container is still alive — later + # E2e log steps can race with compose teardown. Captures container state, + # process tree, resource snapshot, and OOM/segfault evidence so the next + # silent Chrome death leaves a usable trace. + - name: Capture selenium diagnostics on failure + if: ${{ failure() && matrix.suite == 'e2e' }} + run: | + . ci/ciLibrary.source + mkdir -p selenium-diagnostics + # Container state — confirms whether selenium was up at failure time. + dc ps -a > selenium-diagnostics/docker-compose-ps.txt 2>&1 || true + # Process tree inside the selenium container. + dc top selenium > selenium-diagnostics/docker-compose-top.txt 2>&1 || true + # Memory/CPU snapshot — resolve service to container ID since + # `docker stats` does not accept compose service names. + selenium_cid=$(dc ps -q selenium 2>/dev/null || true) + if [[ -n "${selenium_cid}" ]]; then + docker stats --no-stream "${selenium_cid}" > selenium-diagnostics/docker-stats.txt 2>&1 || true + else + echo 'selenium service has no container at failure time' > selenium-diagnostics/docker-stats.txt + fi + # Kernel ring buffer — surface OOM-killer / segfault / chrome lines first, + # then a tail as fallback when the buffer is silent. + sudo dmesg -T 2>/dev/null | grep -iE 'oom|killed|segfault|chrome|selenium' \ + > selenium-diagnostics/dmesg.matches.txt 2>&1 || true + sudo dmesg -T 2>/dev/null | tail -200 > selenium-diagnostics/dmesg.tail.txt 2>&1 || true + # Full selenium logs (also echoed to the job log by the later step, but + # capturing to a file makes them downloadable alongside the rest). + dc logs --no-color selenium > selenium-diagnostics/selenium.log 2>&1 || true + + - name: Upload selenium diagnostics + if: ${{ failure() && matrix.suite == 'e2e' }} + uses: actions/upload-artifact@v7 + with: + name: selenium-diagnostics-${{ needs.setup.outputs.docker_dir }} + path: selenium-diagnostics/ + if-no-files-found: ignore + ## # E2E-specific post-test steps - name: E2e container logs From 0827e36dbc1283d6f8243874c3eb53abd440be58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:43:26 -0400 Subject: [PATCH 12/82] chore(deps): bump openemr/openemr from flex-3.17 to flex-3.17 in /docker/development-insane in the openemr-images group across 1 directory (#11870) Bumps the openemr-images group with 1 update in the /docker/development-insane directory: openemr/openemr. Updates `openemr/openemr` from flex-3.17 to flex-3.17 [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=openemr/openemr&package-manager=docker_compose&previous-version=flex-3.17&new-version=flex-3.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/development-insane/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/development-insane/docker-compose.yml b/docker/development-insane/docker-compose.yml index e7cd4983f547..07f92bfaf736 100644 --- a/docker/development-insane/docker-compose.yml +++ b/docker/development-insane/docker-compose.yml @@ -260,7 +260,7 @@ services: timeout: 5s openemr-edge: restart: always - image: openemr/openemr:flex-edge@sha256:09422098639c806068ce4823478a665dcbc0edd380dc5784afd2e275acfa87a9 + image: openemr/openemr:flex-edge@sha256:cfe271e7f0c5613218c9c38c9521f5f148f94d8d220a9179422dd241c3785501 ports: - 8088:80 - 9088:443 @@ -445,7 +445,7 @@ services: timeout: 5s openemr-edge-redis: restart: always - image: openemr/openemr:flex-edge@sha256:09422098639c806068ce4823478a665dcbc0edd380dc5784afd2e275acfa87a9 + image: openemr/openemr:flex-edge@sha256:cfe271e7f0c5613218c9c38c9521f5f148f94d8d220a9179422dd241c3785501 ports: - 8098:80 - 9098:443 From 3391b88b1259c76dcff9f1fa1ed93986480c1d9a Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 09:24:05 -0400 Subject: [PATCH 13/82] chore(phpstan): drain Carecoordination module class.notFound + method.notFound (#11859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drain `class.notFound` and `method.notFound` baseline entries originating in the Carecoordination zend module. Fourth phase-4 module batch following the eRx drain (#11825). ## Source changes **Controller property native types.** Convert PHPDoc-typed properties on `CarecoordinationController` and `CcdController` to native types (constructor property promotion applied via Rector). PHPStan was resolving docblocks like `@var Documents\Controller\DocumentsController` relative to the `Carecoordination\Controller` namespace, producing `class.notFound` for nonexistent paths. **`@method` tags for Laminas MVC plugins.** `$this->CommonPlugin()` and `$this->Documents()` resolve through Laminas' plugin manager via `__call`. Add explicit `@method` declarations on the controllers that invoke them, plus `@method \Laminas\Http\Request getRequest()` to narrow the request type so `getPost()`/`getQuery()` resolve. **Fix `CcdController::uploadAction()` bug.** The method was calling `$this->updateDocumentCategoryUsingCatname()` on itself, which does not exist on the controller. The sibling `CarecoordinationController::uploadAction()` calls it correctly via `$this->documentsController->getDocumentsTable()->updateDocumentCategoryUsingCatname()` — apply the same call here. Dead since at least 2014; PHPStan would have caught it as `method.notFound` if the entry hadn't been baselined. **Remove dead `CarecoordinationTable::getIssues()`.** Body called `DocumentsTable::getIssues()` which does not exist. Author left an inline `Method not used` TODO; grep confirms no callers. **Remove dead try/catch in `EncounterccdadispatchController::indexAction()`.** The block wrapped only `echo`/`exit` calls that cannot throw. **`getCarecoordinationTable()` / `getDocumentsTable()` return types.** Changing the docblock from `@return object` to a real return type collapses several `method.notFound` entries that were `Cannot call method ... on object`. **DOM narrowing.** `CcdaUserPreferencesTransformer` and `CcdaServiceDocumentRequestorTest` iterate `\DOMNodeList` results and call element-only methods (`insertBefore`, `setAttribute`, `getAttribute`). Add `instanceof \DOMElement` and `instanceof \DOMNode` guards plus a null-check on `lastElementChild` so the type system sees the narrowing. **Content type narrowing in `EncounterccdadispatchController::indexAction()`.** Initialize `$content = ''` up front and call `strval($content)` before substring operations to avoid Rector flip-flop between `RecastingRemovalRector` (full-codebase) and `NullToStrictStringFuncCallArgRector` (single-file). ## Tests Adds `CcdaUserPreferencesTransformerTest` covering `transform()` (sort, truncate, default-fallback) and the getter/setter round-trip. Lifts patch coverage on this PR. ## Counts | Identifier | Before | After | Delta | |---|---:|---:|---:| | `class.notFound` | 237 | 229 | -8 | | `method.notFound` | 184 | 160 | -24 | | `method.nonObject` | 1995 | 1991 | -4 | | `argument.type` | 14973 | 14972 | -1 | | `assign.propertyType` | 388 | 384 | -4 | | `cast.string` | 807 | 806 | -1 | | `missingType.parameter` | 9488 | 9487 | -1 | | `missingType.property` | 6198 | 6195 | -3 | | `missingType.return` | 5913 | 5912 | -1 | | `offsetAccess.nonOffsetAccessible` | 10757 | 10756 | -1 | | `preDec.type` | 9 | 8 | -1 | | `property.notFound` | 135 | 132 | -3 | | `return.type` | 2167 | 2164 | -3 | | `variable.undefined` | 3075 | 3074 | -1 | Caps in `.phpstan/fatal-baseline-caps.php` updated to match. Refs #11792. ## Test plan - [x] `composer phpstan-baseline` regenerates cleanly - [x] `composer phpunit-isolated -- --filter=FatalBaselineCaps` passes - [x] `composer phpcs` clean on edited files - [x] Pre-commit hooks (rector, phpcs, phpstan, codespell, composer-require-checker) all pass --- .phpstan/baseline/argument.type.php | 13 +- .phpstan/baseline/assign.propertyType.php | 20 --- .phpstan/baseline/cast.string.php | 5 - .phpstan/baseline/class.notFound.php | 40 ------ .phpstan/baseline/empty.notAllowed.php | 2 +- .phpstan/baseline/method.nonObject.php | 22 +--- .phpstan/baseline/method.notFound.php | 120 ----------------- .phpstan/baseline/missingType.parameter.php | 5 - .phpstan/baseline/missingType.property.php | 15 --- .phpstan/baseline/missingType.return.php | 5 - .../offsetAccess.nonOffsetAccessible.php | 5 - .../baseline/openemr.exitInCatchOrFinally.php | 2 +- .../baseline/openemr.forbiddenCatchType.php | 2 +- .phpstan/baseline/preDec.type.php | 5 - .phpstan/baseline/property.notFound.php | 15 --- .phpstan/baseline/return.type.php | 15 --- .phpstan/baseline/variable.undefined.php | 5 - .phpstan/fatal-baseline-caps.php | 6 +- .../Controller/CarecoordinationController.php | 29 ++--- .../Controller/CcdController.php | 42 ++---- .../EncounterccdadispatchController.php | 27 ++-- .../Controller/EncountermanagerController.php | 4 + .../Controller/MapperController.php | 3 + .../Controller/SetupController.php | 3 + .../Model/CarecoordinationTable.php | 12 -- .../Model/CcdaUserPreferencesTransformer.php | 12 +- .../CcdaServiceDocumentRequestorTest.php | 8 +- .../CcdaUserPreferencesTransformerTest.php | 123 ++++++++++++++++++ 28 files changed, 190 insertions(+), 375 deletions(-) create mode 100644 tests/Tests/Services/Modules/Carecoordination/Model/CcdaUserPreferencesTransformerTest.php diff --git a/.phpstan/baseline/argument.type.php b/.phpstan/baseline/argument.type.php index a648762ef286..6d2580a40d70 100644 --- a/.phpstan/baseline/argument.type.php +++ b/.phpstan/baseline/argument.type.php @@ -12322,22 +12322,22 @@ 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/autoload_register.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$ccdTable of class Carecoordination\\\\Controller\\\\CcdController constructor expects Carecoordination\\\\Model\\\\CcdTable, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$carecoordinationTable of class Carecoordination\\\\Controller\\\\CarecoordinationController constructor expects Carecoordination\\\\Model\\\\CarecoordinationTable, mixed given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/config/module.config.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$dispatchTable of class Carecoordination\\\\Model\\\\CcdaGenerator constructor expects Carecoordination\\\\Model\\\\EncounterccdadispatchTable, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$ccdTable of class Carecoordination\\\\Controller\\\\CcdController constructor expects Carecoordination\\\\Model\\\\CcdTable, mixed given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/config/module.config.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$generator of class Carecoordination\\\\Listener\\\\CCDAEventsSubscriber constructor expects Carecoordination\\\\Model\\\\CcdaGenerator, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$dispatchTable of class Carecoordination\\\\Model\\\\CcdaGenerator constructor expects Carecoordination\\\\Model\\\\EncounterccdadispatchTable, mixed given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/config/module.config.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$table of class Carecoordination\\\\Controller\\\\CarecoordinationController constructor expects Carecoordination\\\\Model\\\\CarecoordinationTable, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$generator of class Carecoordination\\\\Listener\\\\CCDAEventsSubscriber constructor expects Carecoordination\\\\Model\\\\CcdaGenerator, mixed given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/config/module.config.php', ]; @@ -12726,11 +12726,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$contextNode of method DOMXPath\\:\\:query\\(\\) expects DOMNode\\|null, DOMNameSpaceNode\\|DOMNode given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$array of function reset expects array\\|object, mixed given\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/assign.propertyType.php b/.phpstan/baseline/assign.propertyType.php index 21098ad47ae0..a0c122afd34b 100644 --- a/.phpstan/baseline/assign.propertyType.php +++ b/.phpstan/baseline/assign.propertyType.php @@ -286,26 +286,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Application/src/Application/Listener/Listener.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:\\$carecoordinationTable \\(Carecoordination\\\\Controller\\\\Carecoordination\\\\Model\\\\CarecoordinationTable\\) does not accept Carecoordination\\\\Model\\\\CarecoordinationTable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:\\$documentsController \\(Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController\\) does not accept Documents\\\\Controller\\\\DocumentsController\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:\\$listenerObject \\(Carecoordination\\\\Controller\\\\Application\\\\Listener\\\\Listener\\) does not accept Application\\\\Listener\\\\Listener\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CcdController\\:\\:\\$documentsController \\(Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController\\) does not accept Documents\\\\Controller\\\\DocumentsController\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property Carecoordination\\\\Model\\\\CcdaGlobalsConfiguration\\:\\:\\$ccdaSections \\(array\\) does not accept mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/cast.string.php b/.phpstan/baseline/cast.string.php index 17e0f60e987f..10bc39f5518d 100644 --- a/.phpstan/baseline/cast.string.php +++ b/.phpstan/baseline/cast.string.php @@ -1056,11 +1056,6 @@ 'count' => 32, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', 'count' => 6, diff --git a/.phpstan/baseline/class.notFound.php b/.phpstan/baseline/class.notFound.php index 2023f68b369f..9a3f1cf8c7ab 100644 --- a/.phpstan/baseline/class.notFound.php +++ b/.phpstan/baseline/class.notFound.php @@ -56,46 +56,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Application/src/Application/Listener/Listener.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to method getDocumentsTable\\(\\) on an unknown class Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to method isZipUpload\\(\\) on an unknown class Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to method uploadAction\\(\\) on an unknown class Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:\\$carecoordinationTable has unknown class Carecoordination\\\\Controller\\\\Carecoordination\\\\Model\\\\CarecoordinationTable as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:\\$documentsController has unknown class Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:\\$listenerObject has unknown class Carecoordination\\\\Controller\\\\Application\\\\Listener\\\\Listener as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to method uploadAction\\(\\) on an unknown class Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CcdController\\:\\:\\$documentsController has unknown class Carecoordination\\\\Controller\\\\Documents\\\\Controller\\\\DocumentsController as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; $ignoreErrors[] = [ 'message' => '#^Iterating over an object of an unknown class Installer\\\\Controller\\\\unknown_type\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/empty.notAllowed.php b/.phpstan/baseline/empty.notAllowed.php index 53f90d97759e..116d38e6322a 100644 --- a/.phpstan/baseline/empty.notAllowed.php +++ b/.phpstan/baseline/empty.notAllowed.php @@ -1428,7 +1428,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 11, + 'count' => 10, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/method.nonObject.php b/.phpstan/baseline/method.nonObject.php index c71fb82627c0..05808c133811 100644 --- a/.phpstan/baseline/method.nonObject.php +++ b/.phpstan/baseline/method.nonObject.php @@ -2006,11 +2006,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/Module.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method fetchXmlDocuments\\(\\) on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method updateDocumentCategoryUsingCatname\\(\\) on mixed\\.$#', 'count' => 1, @@ -2022,15 +2017,10 @@ 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot call method updateDocumentCategory\\(\\) on mixed\\.$#', + 'message' => '#^Cannot call method updateDocumentCategoryUsingCatname\\(\\) on mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method date_format\\(\\) on mixed\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method z_xlt\\(\\) on mixed\\.$#', 'count' => 13, @@ -9746,21 +9736,11 @@ 'count' => 2, 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getAttribute\\(\\) on DOMNameSpaceNode\\|DOMNode\\|null\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method item\\(\\) on DOMNodeList\\\\|false\\.$#', 'count' => 3, 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method setAttribute\\(\\) on DOMNameSpaceNode\\|DOMNode\\|null\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method installPatientFixtures\\(\\) on mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/method.notFound.php b/.phpstan/baseline/method.notFound.php index 29584f2110cf..87439c61d733 100644 --- a/.phpstan/baseline/method.notFound.php +++ b/.phpstan/baseline/method.notFound.php @@ -106,111 +106,6 @@ 'count' => 3, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Application/src/Application/Controller/SendtoController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:CommonPlugin\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:Documents\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getPost\\(\\)\\.$#', - 'count' => 13, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getQuery\\(\\)\\.$#', - 'count' => 9, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Carecoordination\\\\Controller\\\\CcdController\\:\\:updateDocumentCategoryUsingCatname\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getPost\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getQuery\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:document_fetch\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:fetch_cat_id\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:fetch_uploaded_documents\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:getDocument\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getPost\\(\\)\\.$#', - 'count' => 11, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getQuery\\(\\)\\.$#', - 'count' => 18, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Carecoordination\\\\Controller\\\\EncountermanagerController\\:\\:CommonPlugin\\(\\)\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getPost\\(\\)\\.$#', - 'count' => 27, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getQuery\\(\\)\\.$#', - 'count' => 13, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getPost\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/MapperController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Laminas\\\\Stdlib\\\\RequestInterface\\:\\:getPost\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/SetupController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method Documents\\\\Model\\\\DocumentsTable\\:\\:getIssues\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method DOMNameSpaceNode\\|DOMNode\\:\\:insertBefore\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method DOMNameSpaceNode\\|DOMNode\\:\\:removeChild\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', -]; $ignoreErrors[] = [ 'message' => '#^Call to an undefined method Ccr\\\\Controller\\\\CcrController\\:\\:CommonPlugin\\(\\)\\.$#', 'count' => 2, @@ -901,21 +796,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../tests/Tests/Services/FHIR/FhirLocationServiceTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method DOMNameSpaceNode\\|DOMNode\\:\\:getAttribute\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method DOMNameSpaceNode\\|DOMNode\\:\\:hasAttribute\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method DOMNameSpaceNode\\|DOMNode\\:\\:setAttribute\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Call to an undefined method OpenEMR\\\\FHIR\\\\SMART\\\\ExternalClinicalDecisionSupport\\\\DecisionSupportInterventionEntity\\:\\:populateServiceWithFhirQuestionnaire\\(\\)\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.parameter.php b/.phpstan/baseline/missingType.parameter.php index 5edc716c9386..0470ef3a88cf 100644 --- a/.phpstan/baseline/missingType.parameter.php +++ b/.phpstan/baseline/missingType.parameter.php @@ -11281,11 +11281,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Carecoordination\\\\Model\\\\CarecoordinationTable\\:\\:getIssues\\(\\) has parameter \\$pid with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Carecoordination\\\\Model\\\\CarecoordinationTable\\:\\:getLabResults\\(\\) has parameter \\$data with no type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.property.php b/.phpstan/baseline/missingType.property.php index 381af458efbf..f1ce8d445897 100644 --- a/.phpstan/baseline/missingType.property.php +++ b/.phpstan/baseline/missingType.property.php @@ -2521,21 +2521,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Application/src/Application/Plugin/CommonPlugin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CcdController\\:\\:\\$carecoordinationTable has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CcdController\\:\\:\\$documentsTable has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property Carecoordination\\\\Controller\\\\CcdController\\:\\:\\$listenerObject has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property Carecoordination\\\\Controller\\\\EncounterccdadispatchController\\:\\:\\$components has no type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.return.php b/.phpstan/baseline/missingType.return.php index 43f22422881a..c22a50ce926c 100644 --- a/.phpstan/baseline/missingType.return.php +++ b/.phpstan/baseline/missingType.return.php @@ -6081,11 +6081,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Carecoordination\\\\Controller\\\\CcdController\\:\\:getDocumentsTable\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Carecoordination\\\\Controller\\\\CcdController\\:\\:importAction\\(\\) has no return type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php index b76b1b8746e9..9c2139676137 100644 --- a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php +++ b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php @@ -16761,11 +16761,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset 0 on mixed\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'encounter\' on mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/openemr.exitInCatchOrFinally.php b/.phpstan/baseline/openemr.exitInCatchOrFinally.php index 92d808ecfe09..4b8621187015 100644 --- a/.phpstan/baseline/openemr.exitInCatchOrFinally.php +++ b/.phpstan/baseline/openemr.exitInCatchOrFinally.php @@ -73,7 +73,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^exit/die inside a catch block swallows the caught exception and aborts the process\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/openemr.forbiddenCatchType.php b/.phpstan/baseline/openemr.forbiddenCatchType.php index 0f8d6c7ed86b..80d6c16a652f 100644 --- a/.phpstan/baseline/openemr.forbiddenCatchType.php +++ b/.phpstan/baseline/openemr.forbiddenCatchType.php @@ -263,7 +263,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^catch \\(Throwable\\) would suppress Error, which is forbidden\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/preDec.type.php b/.phpstan/baseline/preDec.type.php index 724e0cb4aa32..73b611cd98ee 100644 --- a/.phpstan/baseline/preDec.type.php +++ b/.phpstan/baseline/preDec.type.php @@ -1,11 +1,6 @@ '#^Cannot use \\-\\- on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot use \\-\\- on mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/property.notFound.php b/.phpstan/baseline/property.notFound.php index 4509d544ce31..c341a5c821c7 100644 --- a/.phpstan/baseline/property.notFound.php +++ b/.phpstan/baseline/property.notFound.php @@ -166,21 +166,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/custom_modules/oe-module-faxsms/src/EtherFax/EtherFaxClient.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Access to an undefined property DOMNameSpaceNode\\|DOMNode\\:\\:\\$childElementCount\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Access to an undefined property DOMNameSpaceNode\\|DOMNode\\:\\:\\$firstChild\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Access to an undefined property DOMNameSpaceNode\\|DOMNode\\:\\:\\$lastElementChild\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php', -]; $ignoreErrors[] = [ 'message' => '#^Access to an undefined property Carecoordination\\\\Model\\\\ModuleconfigTable\\:\\:\\$applicationTable\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/return.type.php b/.phpstan/baseline/return.type.php index 290ff8deb5a0..81313e12194a 100644 --- a/.phpstan/baseline/return.type.php +++ b/.phpstan/baseline/return.type.php @@ -1026,21 +1026,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Application/src/Application/Model/SendtoTable.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:getCarecoordinationTable\\(\\) should return Carecoordination\\\\Model\\\\CarecoordinationTable but returns Carecoordination\\\\Controller\\\\Carecoordination\\\\Model\\\\CarecoordinationTable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Carecoordination\\\\Controller\\\\CarecoordinationController\\:\\:indexAction\\(\\) should return Laminas\\\\View\\\\Model\\\\ViewModel but returns Laminas\\\\Http\\\\Response\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Carecoordination\\\\Controller\\\\CcdController\\:\\:getCarecoordinationTable\\(\\) should return object but returns mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Carecoordination\\\\Controller\\\\EncounterccdadispatchController\\:\\:getEncounterccdadispatchTable\\(\\) should return Carecoordination\\\\Model\\\\EncounterccdadispatchTable but returns mixed\\.$#', 'count' => 1, @@ -10831,10 +10821,5 @@ 'count' => 1, 'path' => __DIR__ . '/../../tests/Tests/Services/FHIR/QuestionnaireResponse/FhirQuestionnaireResponseFormServiceIntegrationTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method OpenEMR\\\\Tests\\\\Services\\\\Modules\\\\CareCoordination\\\\Model\\\\CcdaServiceDocumentRequestorTest\\:\\:getDocumentGenerationTime\\(\\) should return string but returns mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php', -]; return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; diff --git a/.phpstan/baseline/variable.undefined.php b/.phpstan/baseline/variable.undefined.php index 204dbd8fb365..1c1f095076d7 100644 --- a/.phpstan/baseline/variable.undefined.php +++ b/.phpstan/baseline/variable.undefined.php @@ -10291,11 +10291,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$content might not be defined\\.$#', - 'count' => 6, - 'path' => __DIR__ . '/../../interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$result might not be defined\\.$#', 'count' => 1, diff --git a/.phpstan/fatal-baseline-caps.php b/.phpstan/fatal-baseline-caps.php index d9edb7119c63..00e9c060944f 100644 --- a/.phpstan/fatal-baseline-caps.php +++ b/.phpstan/fatal-baseline-caps.php @@ -35,20 +35,20 @@ return [ 'all' => [ - 'class.notFound.php' => 237, + 'class.notFound.php' => 229, 'classConstant.notFound.php' => 0, 'constant.notFound.php' => 0, 'function.notFound.php' => 0, 'include.fileNotFound.php' => 0, 'includeOnce.fileNotFound.php' => 0, 'interface.notFound.php' => 0, - 'method.notFound.php' => 184, + 'method.notFound.php' => 160, 'require.fileNotFound.php' => 0, 'requireOnce.fileNotFound.php' => 0, 'return.missing.php' => 0, 'staticMethod.notFound.php' => 0, 'trait.notFound.php' => 0, - 'variable.undefined.php' => 3075, + 'variable.undefined.php' => 3074, ], 'confidentNonObject' => [ 'classConstant.nonObject.php' => 0, diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php index 97e812165508..eb7750eea074 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php @@ -28,34 +28,21 @@ use OpenEMR\Core\OEGlobalsBag; use OpenEMR\Services\Cda\CdaValidateDocuments; +/** + * @method \Application\Plugin\CommonPlugin CommonPlugin() + * @method \Documents\Plugin\Documents Documents() + * @method \Laminas\Http\Request getRequest() + */ class CarecoordinationController extends AbstractActionController { - /** - * @var Carecoordination\Model\CarecoordinationTable - */ - private $carecoordinationTable; - - /** - * @var Documents\Controller\DocumentsController - */ - private $documentsController; - - /** - * @var Application\Listener\Listener - */ - private $listenerObject; + private readonly Listener $listenerObject; - /** - * @var string - */ - private $date_format; + private readonly string $date_format; - public function __construct(CarecoordinationTable $table, DocumentsController $documentsController) + public function __construct(private readonly CarecoordinationTable $carecoordinationTable, private readonly DocumentsController $documentsController) { - $this->carecoordinationTable = $table; $this->listenerObject = new Listener(); $this->date_format = ApplicationTable::dateFormat(OEGlobalsBag::getInstance()->get('date_display_format')); - $this->documentsController = $documentsController; } /** diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php index ba7541e4116f..02b65bd9d8bd 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CcdController.php @@ -21,35 +21,21 @@ use Laminas\View\Model\ViewModel; use OpenEMR\Common\Session\SessionWrapperFactory; +/** + * @method \Laminas\Http\Request getRequest() + */ class CcdController extends AbstractActionController { - /** - * @var \Carecoordination\Model\CcdTable - */ - protected $ccdTable; - - protected $carecoordinationTable; - - protected $documentsTable; - - protected $listenerObject; - /** - * @var Documents\Controller\DocumentsController - */ - private $documentsController; + protected Listener $listenerObject; public function __construct( - CcdTable $ccdTable, - CarecoordinationTable $carecoordinationTable, - DocumentsTable $documentsTable, - DocumentsController $documentsController + protected CcdTable $ccdTable, + protected CarecoordinationTable $carecoordinationTable, + protected DocumentsTable $documentsTable, + private readonly DocumentsController $documentsController ) { $this->listenerObject = new Listener(); - $this->ccdTable = $ccdTable; - $this->carecoordinationTable = $carecoordinationTable; - $this->documentsTable = $documentsTable; - $this->documentsController = $documentsController; } /* @@ -79,7 +65,7 @@ public function uploadAction() if ($row['doc_type'] == 'CCD') { $_REQUEST["document_id"] = $row['doc_id']; $this->importAction(); - $this->updateDocumentCategoryUsingCatname($row['doc_type'], $row['doc_id']); + $this->documentsController->getDocumentsTable()->updateDocumentCategoryUsingCatname($row['doc_type'], $row['doc_id']); } } } @@ -115,7 +101,7 @@ public function importAction() $xml_content = $this->getCarecoordinationTable()->getDocument($document_id); $xmltoarray = new \Laminas\Config\Reader\Xml(); - $array = $xmltoarray->fromString((string) $xml_content); + $array = $xmltoarray->fromString($xml_content); $this->getCcdTable()->import($array, $document_id); @@ -132,16 +118,12 @@ public function getCcdTable() { return $this->ccdTable; } - /** - * Table gateway - * @return object - */ - public function getCarecoordinationTable() + public function getCarecoordinationTable(): CarecoordinationTable { return $this->carecoordinationTable; } - public function getDocumentsTable() + public function getDocumentsTable(): DocumentsTable { return $this->documentsTable; } diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php index 938a5e2891b7..7cf320f32f67 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php @@ -30,6 +30,9 @@ use OpenEMR\Cqm\QrdaControllers\QrdaReportController; use XSLTProcessor; +/** + * @method \Laminas\Http\Request getRequest() + */ class EncounterccdadispatchController extends AbstractActionController { protected $data; @@ -78,6 +81,7 @@ public function indexAction() $representedOrganization = $this->getEncounterccdadispatchTable()->getRepresentedOrganization(); + $content = ''; $request = $this->getRequest(); $this->patient_id = $request->getQuery('pid'); $this->encounter_id = $request->getQuery('encounter'); @@ -238,6 +242,7 @@ public function indexAction() // split content if unstructured is included from service. $unstructured = ""; + $content = strval($content); if (substr_count($content, '') === 2) { $d = explode('', $content); $content = $d[0] . ''; @@ -307,25 +312,11 @@ public function indexAction() die(); } - try { - ob_clean(); - if (!empty($_POST['sent_by_app'] ?? '')) { - echo $content; - exit; - } - if (empty($downloadccda)) { - $practice_filename = "CCDA_{$this->patient_id}.xml"; - header("Cache-Control: public"); - header("Content-Description: File Transfer"); - header("Content-Disposition: attachment; filename=" . $practice_filename); - header("Content-Type: application/download"); - header("Content-Transfer-Encoding: binary"); - echo $content; - } - exit; - } catch (\Throwable $e) { - die($e->getMessage()); + ob_clean(); + if (!empty($_POST['sent_by_app'] ?? '')) { + echo $content; } + exit; } /** diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php index d00ceba1960b..c607af61e695 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php @@ -34,6 +34,10 @@ use OpenEMR\Validators\ProcessingResult; use XSLTProcessor; +/** + * @method \Application\Plugin\CommonPlugin CommonPlugin() + * @method \Laminas\Http\Request getRequest() + */ class EncountermanagerController extends AbstractActionController { // TODO: is there a better place for this? These are the values from the applications/sendto/sendto.phtml for diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/MapperController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/MapperController.php index 02a919dc32bd..00e948aa52c2 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/MapperController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/MapperController.php @@ -18,6 +18,9 @@ // TODO: this class appears to be deprecated as nothing else refers to it. It looks like it does the same thing as the SetupController does... // Recommend removing this if it's not used. +/** + * @method \Laminas\Http\Request getRequest() + */ class MapperController extends AbstractActionController { protected $mapperTable; diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/SetupController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/SetupController.php index dae17b67fb7d..103dd0f980ba 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/SetupController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/SetupController.php @@ -16,6 +16,9 @@ use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; +/** + * @method \Laminas\Http\Request getRequest() + */ class SetupController extends AbstractActionController { /** diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php index 372c746192f9..b3b530a16967 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php @@ -1152,18 +1152,6 @@ public function getCategory() return $doc_obj->getCategory(); } - /** - * @param $pid - * @return mixed - */ - public function getIssues($pid) - { - // @todo Beware getIssues() doesn't exist in DocumentTable()! Method not used - $doc_obj = new DocumentsTable(); - $issues = $doc_obj->getIssues($pid); - return $issues; - } - /** * @return string */ diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php index c6a1837790d7..b9585c64951d 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaUserPreferencesTransformer.php @@ -73,11 +73,17 @@ public function transform($content) $structuredBodies = $xpath->query($query); foreach ($structuredBodies as $body) { + if (!$body instanceof \DOMElement) { + continue; + } if (!empty($sortedSections)) { foreach ($sortedSections as $section) { $foundSectionNodes = $xpath->query("n1:component[n1:section/n1:templateId/@root = '" . $section . "']", $body); if ($foundSectionNodes !== false && $foundSectionNodes->length > 0) { foreach ($foundSectionNodes as $node) { + if (!$node instanceof \DOMNode) { + continue; + } // if our found node is already the first child we will just leave it alone and skip over. if ($node !== $body->firstChild) { // if firstChild is empty it will just append @@ -93,7 +99,11 @@ public function transform($content) // component sections until we get to our max number of nodes if ($body->childElementCount && $body->childElementCount > $maxChildren) { for ($i = $body->childElementCount; $i > $maxChildren; --$i) { - $body->removeChild($body->lastElementChild); + $lastElement = $body->lastElementChild; + if ($lastElement === null) { + break; + } + $body->removeChild($lastElement); } } } diff --git a/tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php b/tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php index 82234945e31b..7df5c9c2916a 100644 --- a/tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php +++ b/tests/Tests/Services/Modules/Carecoordination/Model/CcdaServiceDocumentRequestorTest.php @@ -99,7 +99,7 @@ private function getDocumentGenerationTime(\DOMDocument $xml): string $xpath = new DOMXPath($xml); $xpath->registerNamespace('hl7', 'urn:hl7-org:v3'); $effectiveTime = $xpath->query("//hl7:effectiveTime")->item(0); - if ($effectiveTime && $effectiveTime->hasAttribute('value')) { + if ($effectiveTime instanceof \DOMElement && $effectiveTime->hasAttribute('value')) { return $effectiveTime->getAttribute('value'); } else { throw new \RuntimeException('effectiveTime element with value attribute not found in XML'); @@ -114,7 +114,9 @@ private function replaceLatestTimeStamp(\DOMDocument $xml, string $currentTimest $expr = '//*[@value="' . $currentTimestamp . '"]'; $timestampValues = $xpath->query($expr); foreach ($timestampValues as $timestamp) { - $timestamp->setAttribute('value', $newTimeStamp); + if ($timestamp instanceof \DOMElement) { + $timestamp->setAttribute('value', $newTimeStamp); + } } $dateTime = \DateTimeImmutable::createFromFormat("Ymd", $currentTimestamp); @@ -174,6 +176,8 @@ private function replaceRootIdForNodes(DOMXPath $path, DOMXPath $expectedXpath, $currentNodeId = $path->query(".//hl7:id", $currentNode)->item(0); $expectedNodeId = $expectedXpath->query(".//hl7:id", $expectedNode)->item(0); + static::assertInstanceOf(\DOMElement::class, $currentNodeId); + static::assertInstanceOf(\DOMElement::class, $expectedNodeId); $expectedRootId = $expectedNodeId->getAttribute('root'); $currentNodeId->setAttribute('root', $expectedRootId); diff --git a/tests/Tests/Services/Modules/Carecoordination/Model/CcdaUserPreferencesTransformerTest.php b/tests/Tests/Services/Modules/Carecoordination/Model/CcdaUserPreferencesTransformerTest.php new file mode 100644 index 000000000000..4e6156b3ba2b --- /dev/null +++ b/tests/Tests/Services/Modules/Carecoordination/Model/CcdaUserPreferencesTransformerTest.php @@ -0,0 +1,123 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc. + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +declare(strict_types=1); + +namespace OpenEMR\Tests\Services\Modules\CareCoordination\Model; + +use Carecoordination\Model\CcdaUserPreferencesTransformer; +use DOMDocument; +use DOMXPath; +use PHPUnit\Framework\TestCase; + +class CcdaUserPreferencesTransformerTest extends TestCase +{ + private const NS = 'urn:hl7-org:v3'; + private const DOC_OID = '2.16.840.1.113883.10.20.22.1.2'; + private const ALLERGIES_OID = '2.16.840.1.113883.10.20.22.2.6.1'; + private const MEDS_OID = '2.16.840.1.113883.10.20.22.2.1.1'; + private const PROBLEMS_OID = '2.16.840.1.113883.10.20.22.2.5.1'; + + public function testTransformReordersSectionsByPreference(): void + { + $xml = $this->buildCcda([self::ALLERGIES_OID, self::MEDS_OID, self::PROBLEMS_OID]); + + $transformer = new CcdaUserPreferencesTransformer( + maxSections: 0, + sortPreferences: [self::DOC_OID => [self::PROBLEMS_OID, self::MEDS_OID]], + ); + + $this->assertSame( + [self::PROBLEMS_OID, self::MEDS_OID, self::ALLERGIES_OID], + $this->extractSectionOrder($transformer->transform($xml)), + ); + } + + public function testTransformTruncatesToMaxSections(): void + { + $xml = $this->buildCcda([self::ALLERGIES_OID, self::MEDS_OID, self::PROBLEMS_OID]); + + $transformer = new CcdaUserPreferencesTransformer(maxSections: 2); + + $this->assertCount(2, $this->extractSectionOrder($transformer->transform($xml))); + } + + public function testTransformAppliesDefaultSortWhenDocTypeUnknown(): void + { + $xml = $this->buildCcda([self::ALLERGIES_OID, self::MEDS_OID]); + + $transformer = new CcdaUserPreferencesTransformer( + maxSections: 0, + sortPreferences: ['default' => [self::MEDS_OID]], + ); + + $this->assertSame( + [self::MEDS_OID, self::ALLERGIES_OID], + $this->extractSectionOrder($transformer->transform($xml)), + ); + } + + public function testGettersAndSettersRoundTrip(): void + { + $transformer = new CcdaUserPreferencesTransformer(); + + $transformer->setMaxSections(7)->setSortPreferences(['a' => ['b']]); + + $this->assertSame(7, $transformer->getMaxSections()); + $this->assertSame(['a' => ['b']], $transformer->getSortPreferences()); + } + + /** + * @param list $sectionOids + */ + private function buildCcda(array $sectionOids): string + { + $sections = ''; + foreach ($sectionOids as $oid) { + $sections .= "
"; + } + + $doc = self::DOC_OID; + $ns = self::NS; + return << + + + + $sections + + + XML; + } + + /** + * @return list + */ + private function extractSectionOrder(mixed $xml): array + { + $this->assertIsString($xml); + + $dom = new DOMDocument(); + $dom->loadXML($xml); + $xpath = new DOMXPath($dom); + $xpath->registerNamespace('n1', self::NS); + + $nodes = $xpath->query('//n1:structuredBody/n1:component/n1:section/n1:templateId'); + $this->assertNotFalse($nodes); + + $oids = []; + foreach ($nodes as $node) { + if ($node instanceof \DOMElement) { + $oids[] = $node->getAttribute('root'); + } + } + return $oids; + } +} From 6d509f6ff19418c86fea9a1b082bac56bea91d0f Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 09:47:25 -0400 Subject: [PATCH 14/82] fix(bootstrap): replace die() with exception for missing session site_id (#11618) ## Summary - Replace `die("Site ID is missing from session data!")` in `globals.php` with `throw new MissingSiteIdException()` - Introduce `MissingSiteIdException` extending the existing `MissingSiteException`, so callers can catch either the specific session case or the broader "site not identified" family - This allows callers like `BackgroundServiceRunner` that wrap `globals.php` inclusion in `try`/`catch` to handle the failure gracefully and release DB locks, instead of having the process killed mid-execution ## Notes `portal/patient/scripts/app.js:30` checks for the string "Site ID is missing from session data" in AJAX responses. That handler relies on parsing the raw `die()` output, which is already fragile. With this change, uncaught exceptions in production won't produce that exact string in the response body (PHP returns a 500 with no body when `display_errors` is off). The JS handler should be updated separately to use a proper error response format. ## Test plan - [ ] Verify web requests without a session site_id still get a 400 response (the `http_response_code(400)` call is preserved before the throw) - [ ] Verify CLI callers (e.g. `BackgroundServiceRunner`) can catch `MissingSiteIdException` and clean up gracefully - [ ] Verify the patient portal session expiry flow still redirects correctly (may need follow-up for `app.js`) Closes #11616 --------- Co-authored-by: Eric Stern --- interface/globals.php | 3 +- src/Common/System/MissingSiteException.php | 6 ++- src/Common/System/MissingSiteIdException.php | 30 +++++++++++ .../System/MissingSiteIdExceptionTest.php | 51 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/Common/System/MissingSiteIdException.php create mode 100644 tests/Tests/Isolated/Common/System/MissingSiteIdExceptionTest.php diff --git a/interface/globals.php b/interface/globals.php index 0b85deaca660..02a8eba9ec14 100644 --- a/interface/globals.php +++ b/interface/globals.php @@ -269,8 +269,7 @@ $globalsBag->set('srcdir', $srcdir); require_once("$srcdir/auth.inc.php"); } - http_response_code(400); - die("Site ID is missing from session data!"); + throw new \OpenEMR\Common\System\MissingSiteIdException(); } $tmp = $_SERVER['HTTP_HOST']; diff --git a/src/Common/System/MissingSiteException.php b/src/Common/System/MissingSiteException.php index fdc6ce742d83..0137f9748cf1 100644 --- a/src/Common/System/MissingSiteException.php +++ b/src/Common/System/MissingSiteException.php @@ -12,10 +12,12 @@ namespace OpenEMR\Common\System; -class MissingSiteException extends \RuntimeException +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +class MissingSiteException extends BadRequestHttpException { public function __construct(string $message = "Site directory is not configured. OE_SITE_DIR must be set.", int $code = 0, ?\Throwable $previous = null) { - parent::__construct($message, $code, $previous); + parent::__construct($message, code: $code, previous: $previous); } } diff --git a/src/Common/System/MissingSiteIdException.php b/src/Common/System/MissingSiteIdException.php new file mode 100644 index 000000000000..e4c34131bd82 --- /dev/null +++ b/src/Common/System/MissingSiteIdException.php @@ -0,0 +1,30 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +declare(strict_types=1); + +namespace OpenEMR\Common\System; + +class MissingSiteIdException extends MissingSiteException +{ + public function __construct( + string $message = 'Site ID is missing from session data.', + int $code = 0, + ?\Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); + } +} diff --git a/tests/Tests/Isolated/Common/System/MissingSiteIdExceptionTest.php b/tests/Tests/Isolated/Common/System/MissingSiteIdExceptionTest.php new file mode 100644 index 000000000000..841bb467aad9 --- /dev/null +++ b/tests/Tests/Isolated/Common/System/MissingSiteIdExceptionTest.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +declare(strict_types=1); + +namespace OpenEMR\Tests\Isolated\Common\System; + +use OpenEMR\Common\System\MissingSiteException; +use OpenEMR\Common\System\MissingSiteIdException; +use PHPUnit\Framework\TestCase; + +class MissingSiteIdExceptionTest extends TestCase +{ + public function testDefaultMessage(): void + { + $exception = new MissingSiteIdException(); + $this->assertSame('Site ID is missing from session data.', $exception->getMessage()); + } + + public function testCustomMessageAndPrevious(): void + { + $previous = new \RuntimeException('underlying'); + $exception = new MissingSiteIdException('custom', 0, $previous); + $this->assertSame('custom', $exception->getMessage()); + $this->assertSame($previous, $exception->getPrevious()); + } + + public function testCallersCanCatchAsMissingSiteException(): void + { + try { + throw new MissingSiteIdException(); + } catch (MissingSiteException $caught) { + $this->assertSame('Site ID is missing from session data.', $caught->getMessage()); + } + } + + public function testProducesBadRequestHttpStatus(): void + { + $exception = new MissingSiteIdException(); + $this->assertSame(400, $exception->getStatusCode()); + } +} From 4af7d3a59601fb691027e4556a98443b0c82287d Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 10:47:16 -0400 Subject: [PATCH 15/82] perf(ci): restore file timestamps to enable PHPStan caching (#10387) ## Summary Fixes #10386 PHPStan's container cache (Nette DI) validates against file modification times. Git doesn't preserve mtimes, so a freshly checked out file always looks newer than its cached metadata, invalidating the entire container and forcing a full re-analysis of all ~4000 files even when the result cache restored cleanly. ## Approach The expensive parts of fixing this are (1) a full-history clone, which `git-restore-mtime` needs to walk, and (2) installing the tool itself. Both are pure waste on a cold cache miss. So this PR makes them conditional on whether a cache was actually restored: - Replace `actions/cache@v5` with `actions/cache/restore@v5` + `actions/cache/save@v5` so the restore step exposes a `cache-hit` / `cache-matched-key` output. - When (and only when) a cache was restored: install `git-restore-mtime` from apt, `git fetch --unshallow`, and walk the history to restore mtimes. - Save the cache on `always()` so partial results from a failed analyze still seed the next run. Cold misses pay zero new overhead vs. before this PR. Warm hits pay an unshallow + mtime walk to make the restored cache validate. ## Testing The first run after merge is still cold-cache (no savings). Compare runtime on the second and third runs of the same branch to confirm the cache is now effective. --- .github/workflows/phpstan.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index e0c7ea0119f5..cac29fe8fd88 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -48,14 +48,30 @@ jobs: | .version ' composer.lock) printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT" - - name: PHPStan Cache + - name: Restore PHPStan Cache # https://phpstan.org/user-guide/result-cache - uses: actions/cache@v5 + id: phpstan-cache + uses: actions/cache/restore@v5 with: path: tmp-phpstan # same as in phpstan.neon key: phpstan-result-cache-${{ steps.phpstan-version.outputs.version }}-${{ hashFiles('phpstan.neon.dist', '.phpstan/**', 'tests/PHPStan/**') }}-${{ github.run_id }} restore-keys: | phpstan-result-cache-${{ steps.phpstan-version.outputs.version }}-${{ hashFiles('phpstan.neon.dist', '.phpstan/**', 'tests/PHPStan/**') }}- + # PHPStan's container cache uses file mtimes for validation. Git doesn't + # preserve mtimes, so without restoration every cached file looks stale + # and the cache is effectively useless. Only worth doing when we actually + # restored a cache - on a cold miss this is pure overhead. + - name: Restore file timestamps + if: steps.phpstan-cache.outputs.cache-matched-key != '' + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends git-restore-mtime + # Guard against running on a non-shallow checkout - --unshallow errors + # with "does not make sense" if history is already complete. + if [ -f .git/shallow ]; then + git fetch --unshallow --quiet + fi + git restore-mtime --skip-missing --quiet - name: PHPStan Diagnose run: vendor/bin/phpstan --memory-limit=8G diagnose # Use CI config which sets reportUnmatchedIgnoredErrors: false so this step @@ -86,6 +102,15 @@ jobs: - name: Regenerate PHPStan Baseline if: always() run: composer phpstan-baseline + # Save after baseline regeneration so the cache captures the full set of + # PHPStan invocations for the run. Even on failure - partial cache still + # speeds up the next run. + - name: Save PHPStan Cache + if: always() + uses: actions/cache/save@v5 + with: + path: tmp-phpstan + key: ${{ steps.phpstan-cache.outputs.cache-primary-key }} # Check for baseline drift. Only meaningful when analysis passed - if analysis # failed, the baseline will differ due to the new errors. - name: Ensure baseline is stable From c141a72cf8898785726a28cc4ffbc9319b5034f4 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 11:56:09 -0400 Subject: [PATCH 16/82] chore(phpstan): drain edihistory baseline (method.notFound, variable.undefined) (#11878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Refs #11792. Drains 19 `method.notFound` and 10 `variable.undefined` baseline entries in `library/edihistory/` without changing runtime behavior. - Type `csv_check_x12_obj()` return as `edih_x12_file|false` so PHPStan can narrow callers across the 11 sibling html/error/segments files. Replace the now-redundant `'edih_x12_file' == \$obj::class` checks with `\$obj !== false`. - Pre-initialize accumulator variables in the 271 and 277 segment loops (`\$cls`, `\$loopid`, plus segment-specific scalars). Limited to files where the variables are pure accumulators — `edih_997_error.php` etc. use `isset()` as a "did this segment appear?" check, so pre-initializing would alter behavior. - Decrement caps in `.phpstan/fatal-baseline-caps.php`: - `method.notFound.php`: 160 → 141 - `variable.undefined.php`: 3074 → 3064 - Regenerate `.phpstan/baseline/*.php` (side-effect drains in `argument.type`, `booleanAnd.leftAlwaysTrue`, `encapsedStringPart.nonString`, etc.). ## Test plan - [x] `composer phpstan` passes (via pre-commit hook) - [x] `composer phpunit-isolated -- --filter FatalBaselineCapsIsolatedTest` — 19/19 pass - [ ] CI green --- .phpstan/baseline/argument.type.php | 35 ++++--- .phpstan/baseline/binaryOp.invalid.php | 7 +- .../baseline/booleanAnd.leftAlwaysTrue.php | 22 +---- .../baseline/encapsedStringPart.nonString.php | 10 -- .../baseline/function.alreadyNarrowedType.php | 2 +- .phpstan/baseline/method.notFound.php | 95 ------------------- .phpstan/baseline/missingType.parameter.php | 10 -- .../offsetAccess.nonOffsetAccessible.php | 5 - .phpstan/baseline/phpDoc.parseError.php | 5 - .phpstan/baseline/return.type.php | 5 - .phpstan/baseline/variable.undefined.php | 50 ---------- .phpstan/fatal-baseline-caps.php | 4 +- library/edihistory/edih_271_html.php | 6 +- library/edihistory/edih_277_html.php | 8 +- library/edihistory/edih_278_html.php | 2 +- library/edihistory/edih_835_html.php | 2 +- library/edihistory/edih_997_error.php | 2 +- library/edihistory/edih_csv_inc.php | 51 ++++------ library/edihistory/edih_segments.php | 4 +- 19 files changed, 62 insertions(+), 263 deletions(-) diff --git a/.phpstan/baseline/argument.type.php b/.phpstan/baseline/argument.type.php index 6d2580a40d70..c6b3371e0ee4 100644 --- a/.phpstan/baseline/argument.type.php +++ b/.phpstan/baseline/argument.type.php @@ -23833,7 +23833,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 35, + 'count' => 34, 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', ]; $ignoreErrors[] = [ @@ -23858,7 +23858,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 26, + 'count' => 25, 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', ]; $ignoreErrors[] = [ @@ -23888,7 +23888,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 55, + 'count' => 54, 'path' => __DIR__ . '/../../library/edihistory/edih_278_html.php', ]; $ignoreErrors[] = [ @@ -23918,7 +23918,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', ]; $ignoreErrors[] = [ @@ -23936,6 +23936,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_997_error.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$filepath of function csv_check_x12_obj expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../library/edihistory/edih_997_error.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$separator of function explode expects non\\-empty\\-string, mixed given\\.$#', 'count' => 8, @@ -24201,11 +24206,6 @@ 'count' => 4, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$filename of function csv_check_filepath expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$filename of function filesize expects string, mixed given\\.$#', 'count' => 2, @@ -24351,11 +24351,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$type of function csv_check_filepath expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#', 'count' => 2, @@ -24371,6 +24366,11 @@ 'count' => 3, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_parse.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$filepath of function csv_check_x12_obj expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../library/edihistory/edih_csv_parse.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$separator of function explode expects non\\-empty\\-string, mixed given\\.$#', 'count' => 45, @@ -24531,6 +24531,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_io.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$clm01 of method edih_x12_file\\:\\:edih_x12_transaction\\(\\) expects string, mixed given\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, mixed given\\.$#', 'count' => 1, @@ -24598,7 +24603,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#', - 'count' => 2, + 'count' => 3, 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/binaryOp.invalid.php b/.phpstan/baseline/binaryOp.invalid.php index a36b651e4318..ae5f6eba346c 100644 --- a/.phpstan/baseline/binaryOp.invalid.php +++ b/.phpstan/baseline/binaryOp.invalid.php @@ -10853,7 +10853,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between mixed and \' \' results in an error\\.$#', - 'count' => 18, + 'count' => 17, 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', ]; $ignoreErrors[] = [ @@ -11651,11 +11651,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and "\\\\n"\\|"\\\\r\\\\n" results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between \'edih_upload_files\\: …\' and mixed results in an error\\.$#', 'count' => 4, diff --git a/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php b/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php index d47edf745277..55ca50799ca4 100644 --- a/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php +++ b/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php @@ -28,34 +28,14 @@ ]; $ignoreErrors[] = [ 'message' => '#^Left side of && is always true\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Left side of && is always true\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../library/edihistory/edih_278_html.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Left side of && is always true\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Left side of && is always true\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_997_error.php', -]; $ignoreErrors[] = [ 'message' => '#^Left side of && is always true\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_archive.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Left side of && is always true\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; $ignoreErrors[] = [ 'message' => '#^Left side of && is always true\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/encapsedStringPart.nonString.php b/.phpstan/baseline/encapsedStringPart.nonString.php index 3c0f2126e234..d284eba1cf4b 100644 --- a/.phpstan/baseline/encapsedStringPart.nonString.php +++ b/.phpstan/baseline/encapsedStringPart.nonString.php @@ -3646,11 +3646,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/codes/edih_997_codes.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$loopid \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$fn \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 1, @@ -3826,11 +3821,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$filepath \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$filetype \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 2, diff --git a/.phpstan/baseline/function.alreadyNarrowedType.php b/.phpstan/baseline/function.alreadyNarrowedType.php index 4a43a783d1f2..c5a809f78bda 100644 --- a/.phpstan/baseline/function.alreadyNarrowedType.php +++ b/.phpstan/baseline/function.alreadyNarrowedType.php @@ -112,7 +112,7 @@ 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', ]; $ignoreErrors[] = [ - 'message' => '#^Call to function is_array\\(\\) with list\\ will always evaluate to true\\.$#', + 'message' => '#^Call to function is_array\\(\\) with list\\ will always evaluate to true\\.$#', 'count' => 2, 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', ]; diff --git a/.phpstan/baseline/method.notFound.php b/.phpstan/baseline/method.notFound.php index 87439c61d733..5f8c4813c25b 100644 --- a/.phpstan/baseline/method.notFound.php +++ b/.phpstan/baseline/method.notFound.php @@ -221,101 +221,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/classes/smtp/sasl.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_envelopes\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_message\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_envelopes\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_message\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_envelopes\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_278_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_message\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_278_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_delimiters\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_filename\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_x12_envelopes\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_x12_slice\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_x12_transaction\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_delimiters\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_envelopes\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_filename\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_message\\(\\)\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_segments\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_type\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_x12_slice\\(\\)\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method object\\:\\:edih_x12_transaction\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', -]; $ignoreErrors[] = [ 'message' => '#^Call to an undefined method object\\:\\:_compile_file\\(\\)\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.parameter.php b/.phpstan/baseline/missingType.parameter.php index 0470ef3a88cf..da95a4444bf0 100644 --- a/.phpstan/baseline/missingType.parameter.php +++ b/.phpstan/baseline/missingType.parameter.php @@ -24121,16 +24121,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Function csv_check_x12_obj\\(\\) has parameter \\$filepath with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Function csv_check_x12_obj\\(\\) has parameter \\$type with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', -]; $ignoreErrors[] = [ 'message' => '#^Function csv_convert_bytes\\(\\) has parameter \\$bytes with no type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php index 9c2139676137..2dcdb70e7d20 100644 --- a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php +++ b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php @@ -32911,11 +32911,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_278_html.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'ST\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_835_html.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'e\' on mixed\\.$#', 'count' => 3, diff --git a/.phpstan/baseline/phpDoc.parseError.php b/.phpstan/baseline/phpDoc.parseError.php index 6d2bc15bf8f5..93783c2ee39e 100644 --- a/.phpstan/baseline/phpDoc.parseError.php +++ b/.phpstan/baseline/phpDoc.parseError.php @@ -1578,11 +1578,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^PHPDoc tag @param has invalid value \\(string filepath or filename\\)\\: Unexpected token "filepath", expected variable at offset 109 on line 6$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', -]; $ignoreErrors[] = [ 'message' => '#^PHPDoc tag @param has invalid value \\(string\\)\\: Unexpected token "\\\\n \\* ", expected variable at offset 107 on line 6$#', 'count' => 1, diff --git a/.phpstan/baseline/return.type.php b/.phpstan/baseline/return.type.php index 81313e12194a..14a47d815132 100644 --- a/.phpstan/baseline/return.type.php +++ b/.phpstan/baseline/return.type.php @@ -1676,11 +1676,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Function csv_check_x12_obj\\(\\) should return object but returns false\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_csv_inc.php', -]; $ignoreErrors[] = [ 'message' => '#^Function csv_dirfile_list\\(\\) should return array but returns false\\.$#', 'count' => 2, diff --git a/.phpstan/baseline/variable.undefined.php b/.phpstan/baseline/variable.undefined.php index 1c1f095076d7..c5009077aceb 100644 --- a/.phpstan/baseline/variable.undefined.php +++ b/.phpstan/baseline/variable.undefined.php @@ -13406,56 +13406,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/display_help_icon_inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Undefined variable\\: \\$ebo7$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$cls might not be defined\\.$#', - 'count' => 26, - 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$dmg03 might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$loopid might not be defined\\.$#', - 'count' => 57, - 'path' => __DIR__ . '/../../library/edihistory/edih_271_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$bht might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$cls might not be defined\\.$#', - 'count' => 42, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$loopid might not be defined\\.$#', - 'count' => 33, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$qtystr might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$sc204 might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$sc304 might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_277_html.php', -]; $ignoreErrors[] = [ 'message' => '#^Undefined variable\\: \\$dtp03$#', 'count' => 1, diff --git a/.phpstan/fatal-baseline-caps.php b/.phpstan/fatal-baseline-caps.php index 00e9c060944f..29d55c6c8ea2 100644 --- a/.phpstan/fatal-baseline-caps.php +++ b/.phpstan/fatal-baseline-caps.php @@ -42,13 +42,13 @@ 'include.fileNotFound.php' => 0, 'includeOnce.fileNotFound.php' => 0, 'interface.notFound.php' => 0, - 'method.notFound.php' => 160, + 'method.notFound.php' => 141, 'require.fileNotFound.php' => 0, 'requireOnce.fileNotFound.php' => 0, 'return.missing.php' => 0, 'staticMethod.notFound.php' => 0, 'trait.notFound.php' => 0, - 'variable.undefined.php' => 3074, + 'variable.undefined.php' => 3064, ], 'confidentNonObject' => [ 'classConstant.nonObject.php' => 0, diff --git a/library/edihistory/edih_271_html.php b/library/edihistory/edih_271_html.php index bcab4faa9bd5..e0ceb147d9be 100644 --- a/library/edihistory/edih_271_html.php +++ b/library/edihistory/edih_271_html.php @@ -72,6 +72,10 @@ function edih_271_transaction_html($obj271, $bht03) $dep_eb_html = ""; // $ebct = 0; + $cls = ''; + $loopid = ''; + $dmg03 = ''; + $ebo7 = ''; // $trns_ct = count($trans); for ($i = 0; $i < $trns_ct; $i++) { @@ -568,7 +572,7 @@ function edih_271_html($filename, $bht03 = '') // if ($filename) { $obj271 = csv_check_x12_obj($filename, 'f271'); - if ('edih_x12_file' == $obj271::class) { + if ($obj271 !== false) { if ($bht03) { // particular transaction $html_str .= edih_271_transaction_html($obj271, $bht03); diff --git a/library/edihistory/edih_277_html.php b/library/edihistory/edih_277_html.php index eed01d2e24c3..24bf9831bd48 100644 --- a/library/edihistory/edih_277_html.php +++ b/library/edihistory/edih_277_html.php @@ -76,6 +76,12 @@ function edih_277_transaction_html($obj277, $bht03, $accordion = false) $dep_nm1_html = ""; $sbr_stc_html = ""; $dep_stc_html = ""; + $cls = ''; + $loopid = ''; + $bht = ''; + $qtystr = ''; + $sc204 = ''; + $sc304 = ''; // $trns_ct = count($trans); for ($i = 0; $i < $trns_ct; $i++) { @@ -546,7 +552,7 @@ function edih_277_html($filename, $bht03 = '') if ($fn) { $obj277 = csv_check_x12_obj($fn, 'f277'); - if ($obj277 && 'edih_x12_file' == $obj277::class) { + if ($obj277 !== false) { if ($bht03) { // particular transaction $html_str .= edih_277_transaction_html($obj277, $bht03); diff --git a/library/edihistory/edih_278_html.php b/library/edihistory/edih_278_html.php index 23363ca76a65..03fd553f9051 100644 --- a/library/edihistory/edih_278_html.php +++ b/library/edihistory/edih_278_html.php @@ -856,7 +856,7 @@ function edih_278_html($filename, $bht03 = '') return $html_str; } else { $obj278 = csv_check_x12_obj($filename, 'f278'); - if ($obj278 && 'edih_x12_file' == $obj278::class) { + if ($obj278 !== false) { if ($bht03) { // particular transaction $html_str .= edih_278_transaction_html($obj278, $bht03); diff --git a/library/edihistory/edih_835_html.php b/library/edihistory/edih_835_html.php index e49ad85e4e0d..2cc7b497e9c0 100644 --- a/library/edihistory/edih_835_html.php +++ b/library/edihistory/edih_835_html.php @@ -1570,7 +1570,7 @@ function edih_835_html($filename, $trace = '', $clm01 = '', $summary = false) // if (trim($filename)) { $obj835 = csv_check_x12_obj($filename, 'f835'); - if ($obj835 && 'edih_x12_file' == $obj835::class) { + if ($obj835 !== false) { $fn = $obj835->edih_filename(); $delims = $obj835->edih_delimiters(); $env_ar = $obj835->edih_x12_envelopes(); diff --git a/library/edihistory/edih_997_error.php b/library/edihistory/edih_997_error.php index 45d0bb8857c1..ccef5b51a95c 100644 --- a/library/edihistory/edih_997_error.php +++ b/library/edihistory/edih_997_error.php @@ -341,7 +341,7 @@ function edih_997_error($filepath) $html_str = ''; // $obj997 = csv_check_x12_obj($filepath, 'f997'); - if ($obj997 && ('edih_x12_file' == $obj997::class)) { + if ($obj997 !== false) { $data = edih_997_errdata($obj997); $html_str .= edih_997_err_report($data); } else { diff --git a/library/edihistory/edih_csv_inc.php b/library/edihistory/edih_csv_inc.php index ca7696b02ccf..267773496a47 100644 --- a/library/edihistory/edih_csv_inc.php +++ b/library/edihistory/edih_csv_inc.php @@ -577,44 +577,33 @@ function csv_clear_tmpdir() * * @uses csv_check_filepath() * - * @param string filepath or filename - * @parm string file x12 type - * @return object edih_x12_file class + * @param string $filepath filepath or filename + * @param string $type file x12 type + * @return edih_x12_file|false */ -function csv_check_x12_obj($filepath, $type = '') +function csv_check_x12_obj($filepath, $type = ''): edih_x12_file|false { - // - $x12obj = false; - $ok = false; - // $fp = csv_check_filepath($filepath, $type); - // - if ($fp) { - $x12obj = new edih_x12_file($fp); - if ('edih_x12_file' == $x12obj::class) { - if ($x12obj->edih_valid() == 'ovigs') { - $ok = count($x12obj->edih_segments()); - $ok = ($ok) ? count($x12obj->edih_envelopes()) : false; - $ok = ($ok) ? count($x12obj->edih_delimiters()) : false; - if (!$ok) { - csv_edihist_log("csv_check_x12_obj: object missing properties [$filepath]"); - csv_edihist_log($x12obj->edih_message()); - return false; - } - } else { - csv_edihist_log("csv_check_x12_obj: invalid object $filepath"); - return false; - } - } else { - csv_edihist_log("csv_check_x12_obj: object not edih_x12_file $filepath"); - return false; - } - } else { + if (!$fp) { csv_edihist_log("csv_check_x12_obj: invalid file path $filepath"); return false; } - // + $x12obj = new edih_x12_file($fp); + if ($x12obj->edih_valid() != 'ovigs') { + csv_edihist_log("csv_check_x12_obj: invalid object $filepath"); + return false; + } + + $ok = count($x12obj->edih_segments()) + && count($x12obj->edih_envelopes()) + && count($x12obj->edih_delimiters()); + if (!$ok) { + csv_edihist_log("csv_check_x12_obj: object missing properties [$filepath]"); + csv_edihist_log($x12obj->edih_message()); + return false; + } + return $x12obj; } diff --git a/library/edihistory/edih_segments.php b/library/edihistory/edih_segments.php index 8f0a086fe709..1d866403a7c3 100644 --- a/library/edihistory/edih_segments.php +++ b/library/edihistory/edih_segments.php @@ -1093,7 +1093,7 @@ function edih_display_text($filepath, $filetype = '', $claimid = '', $trace = fa // verify x12 file $x12obj = csv_check_x12_obj($filepath, $ft); // - if ($x12obj && 'edih_x12_file' == $x12obj::class) { + if ($x12obj !== false) { $ftype = $x12obj->edih_type(); $ft = csv_file_type($ftype); $delims = $x12obj->edih_delimiters(); @@ -1109,7 +1109,7 @@ function edih_display_text($filepath, $filetype = '', $claimid = '', $trace = fa return $str_html; } - if (!is_array($segs_ar) || !count($segs_ar)) { + if (count($segs_ar) === 0) { // unknown error $str_html = "

unknown error retrieving segments for " . text($fn) . "

" . PHP_EOL; $str_html .= $x12obj->edih_message() . PHP_EOL; From f4fe65bb8cdf3f7ac4f304b4b5b01510305f53f7 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 12:00:29 -0400 Subject: [PATCH 17/82] fix(portal): drain PHPStan class.notFound baseline for portal/patient (#11877) ## Summary Drains 45 entries from the PHPStan `class.notFound` fatal-category baseline by correcting bogus PHPDoc `@var` and `@param` tokens in portal/patient files. The cap is lowered from 237 to 192 in the same commit. Most fixes replace SQL types (`date`, `longtext`, `datetime`, `char`, `blob`), English description words, and `unknown` with correct PHP types. Editing `IDaoMap2::AddMap` / `SetFetchingStrategy` propagates via `{@inheritdoc}` to every implementing Map class. In `GlobalConfig::GetRenderEngine`, the dynamically instantiated render engine is now narrowed via `assert($engine instanceof IRenderEngine)` so the subsequent `assign()` / `display()` calls resolve against the interface instead of plain `object` (avoiding new `method.notFound` entries). Refs openemr/openemr#11792. Non-overlapping with the WIP in #11859 (Carecoordination zend module). ## Test plan - [x] `composer phpstan-baseline` regenerates clean - [x] Pre-commit hooks pass (phpcs, phpstan, rector, composer-require-checker) - [ ] CI green, including `FatalBaselineCapsIsolatedTest` --- .phpstan/baseline/argument.type.php | 23 +- .phpstan/baseline/assign.propertyType.php | 80 +------ .phpstan/baseline/binaryOp.invalid.php | 15 -- .phpstan/baseline/booleanNot.alwaysFalse.php | 10 - .phpstan/baseline/class.notFound.php | 225 ------------------ .phpstan/baseline/if.alwaysTrue.php | 2 +- .../offsetAccess.nonOffsetAccessible.php | 35 +-- .phpstan/baseline/property.defaultValue.php | 20 -- .phpstan/baseline/return.type.php | 5 - .phpstan/fatal-baseline-caps.php | 2 +- portal/patient/_global_config.php | 24 +- .../fwk/libs/verysimple/Phreeze/IDaoMap2.php | 2 +- .../libs/Model/DAO/OnsiteActivityViewDAO.php | 14 +- .../libs/Model/DAO/OnsiteDocumentDAO.php | 10 +- .../Model/DAO/OnsitePortalActivityDAO.php | 12 +- portal/patient/libs/Model/DAO/PatientDAO.php | 8 +- portal/patient/libs/Model/DAO/UserDAO.php | 10 +- 17 files changed, 62 insertions(+), 435 deletions(-) diff --git a/.phpstan/baseline/argument.type.php b/.phpstan/baseline/argument.type.php index c6b3371e0ee4..f342d0324d0e 100644 --- a/.phpstan/baseline/argument.type.php +++ b/.phpstan/baseline/argument.type.php @@ -27387,23 +27387,18 @@ 'path' => __DIR__ . '/../../portal/messaging/secure_chat.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$appRootUrl of class GenericRouter constructor expects string, root given\\.$#', + 'message' => '#^Parameter \\#2 \\$observer of class Phreezer constructor expects Observable\\|null, ObserveToSmarty given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/_global_config.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$class of function class_exists expects string, specify given\\.$#', + 'message' => '#^Parameter \\#2 \\$value of method IRenderEngine\\:\\:assign\\(\\) expects VARIANT, mixed given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/_global_config.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$observer of class Phreezer constructor expects Observable\\|null, ObserveToSmarty given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$routeMap of class GenericRouter constructor expects array, routemap given\\.$#', - 'count' => 1, + 'message' => '#^Parameter \\#2 \\$value of method IRenderEngine\\:\\:assign\\(\\) expects VARIANT, string given\\.$#', + 'count' => 2, 'path' => __DIR__ . '/../../portal/patient/_global_config.php', ]; $ignoreErrors[] = [ @@ -28731,21 +28726,11 @@ 'count' => 4, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$default of method AppBasePortalController\\:\\:SafeGetVal\\(\\) expects string, date given\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#3 \\$default of method AppBasePortalController\\:\\:SafeGetVal\\(\\) expects string, int given\\.$#', 'count' => 4, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$default of method AppBasePortalController\\:\\:SafeGetVal\\(\\) expects string, longtext given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#3 \\$default of method AppBasePortalController\\:\\:SafeGetVal\\(\\) expects string, mixed given\\.$#', 'count' => 49, diff --git a/.phpstan/baseline/assign.propertyType.php b/.phpstan/baseline/assign.propertyType.php index a0c122afd34b..eeeae647df0e 100644 --- a/.phpstan/baseline/assign.propertyType.php +++ b/.phpstan/baseline/assign.propertyType.php @@ -397,22 +397,7 @@ 'path' => __DIR__ . '/../../library/smarty_legacy/smarty/Smarty_Legacy.class.php', ]; $ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$APP_ROOT \\(app\\) does not accept string\\|false\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_app_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$ROUTE_MAP \\(routemap\\) does not accept array\\\\|bool\\|string\\>\\>\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_app_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$TEMPLATE_ENGINE \\(specify\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_app_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$TEMPLATE_PATH \\(template\\) does not accept mixed\\.$#', + 'message' => '#^Static property GlobalConfig\\:\\:\\$APP_ROOT \\(string\\) does not accept string\\|false\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/_app_config.php', ]; @@ -432,12 +417,7 @@ 'path' => __DIR__ . '/../../portal/patient/_machine_config.php', ]; $ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$ROOT_URL \\(root\\) does not accept string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../portal/patient/_machine_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$WEB_ROOT \\(root\\) does not accept mixed\\.$#', + 'message' => '#^Static property GlobalConfig\\:\\:\\$WEB_ROOT \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/_machine_config.php', ]; @@ -536,11 +516,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$AuthorizeSignedTime \\(date\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$AuthorizedSignature \\(string\\) does not accept mixed\\.$#', 'count' => 1, @@ -551,11 +526,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$CreateDate \\(timestamp\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$DenialReason \\(string\\) does not accept mixed\\.$#', 'count' => 1, @@ -587,12 +557,12 @@ 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$FullDocument \\(blob\\) does not accept mixed\\.$#', + 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$FullDocument \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$FullDocument \\(blob\\) does not accept null\\.$#', + 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$FullDocument \\(string\\) does not accept null\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', ]; @@ -606,11 +576,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$PatientSignedTime \\(date\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$Pid \\(int\\) does not accept mixed\\.$#', 'count' => 1, @@ -621,11 +586,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$ReviewDate \\(date\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsiteDocumentController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$TemplateData \\(string\\) does not accept mixed\\.$#', 'count' => 1, @@ -636,11 +596,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$ActionTakenTime \\(date\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$ActionUser \\(int\\) does not accept mixed\\.$#', 'count' => 1, @@ -652,17 +607,12 @@ 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Checksum \\(longtext\\) does not accept mixed\\.$#', + 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Checksum \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Date \\(date\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Narrative \\(longtext\\) does not accept mixed\\.$#', + 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Narrative \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', ]; @@ -687,12 +637,12 @@ 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$TableAction \\(longtext\\) does not accept mixed\\.$#', + 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$TableAction \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$TableArgs \\(longtext\\) does not accept mixed\\.$#', + 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$TableArgs \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/OnsitePortalActivityController.php', ]; @@ -742,12 +692,7 @@ 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Date \\(date\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Dob \\(date\\) does not accept mixed\\.$#', + 'message' => '#^Property PatientDAO\\:\\:\\$Dob \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', ]; @@ -842,7 +787,7 @@ 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Occupation \\(longtext\\) does not accept mixed\\.$#', + 'message' => '#^Property PatientDAO\\:\\:\\$Occupation \\(string\\) does not accept mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', ]; @@ -911,11 +856,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Regdate \\(date\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Controller/PatientController.php', -]; $ignoreErrors[] = [ 'message' => '#^Property PatientDAO\\:\\:\\$Religion \\(string\\) does not accept mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/binaryOp.invalid.php b/.phpstan/baseline/binaryOp.invalid.php index ae5f6eba346c..7ee3f0edf6ee 100644 --- a/.phpstan/baseline/binaryOp.invalid.php +++ b/.phpstan/baseline/binaryOp.invalid.php @@ -13426,26 +13426,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/messaging/secure_chat.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'/fwk/libs\' results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_app_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'\\:\'\\|\';\' results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_app_config.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between mixed and \'\\.\' results in an error\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/_global_config.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'\\.php\' results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between non\\-falsy\\-string and mixed results in an error\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/booleanNot.alwaysFalse.php b/.phpstan/baseline/booleanNot.alwaysFalse.php index 772cb48d526f..60e38b98c264 100644 --- a/.phpstan/baseline/booleanNot.alwaysFalse.php +++ b/.phpstan/baseline/booleanNot.alwaysFalse.php @@ -121,16 +121,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/templates/address_save_handler.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Negated boolean expression is always false\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_app_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Negated boolean expression is always false\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; $ignoreErrors[] = [ 'message' => '#^Negated boolean expression is always false\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/class.notFound.php b/.phpstan/baseline/class.notFound.php index 9a3f1cf8c7ab..bdbd2a191f5f 100644 --- a/.phpstan/baseline/class.notFound.php +++ b/.phpstan/baseline/class.notFound.php @@ -231,66 +231,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/messaging/secure_chat.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to method assign\\(\\) on an unknown class specify\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Instantiated class specify not found\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$APP_ROOT has unknown class app as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$CONVERT_NULL_TO_EMPTYSTRING has unknown class Setting as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$DEBUG_MODE has unknown class set as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$DEFAULT_ACTION has unknown class default as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$ROOT_URL has unknown class root as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$ROUTE_MAP has unknown class routemap as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$TEMPLATE_CACHE_PATH has unknown class template as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$TEMPLATE_ENGINE has unknown class specify as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$TEMPLATE_PATH has unknown class template as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property GlobalConfig\\:\\:\\$WEB_ROOT has unknown class root as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; $ignoreErrors[] = [ 'message' => '#^Method parseCSV\\:\\:_enclose_value\\(\\) has invalid return type Processed\\.$#', 'count' => 1, @@ -396,11 +336,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/fwk/libs/verysimple/Phreeze/DataSet.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\$property of method IDaoMap2\\:\\:SetFetchingStrategy\\(\\) has invalid type unknown\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/fwk/libs/verysimple/Phreeze/IDaoMap2.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\$value of method MockRouter\\:\\:SetUri\\(\\) has invalid type unknown_type\\.$#', 'count' => 1, @@ -481,166 +416,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/fwk/libs/verysimple/String/VerySimpleStringUtil.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteActivityViewDAO\\:\\:\\$ActionTakenTime has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteActivityViewDAO\\:\\:\\$Checksum has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteActivityViewDAO\\:\\:\\$Date has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteActivityViewDAO\\:\\:\\$Dob has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteActivityViewDAO\\:\\:\\$Narrative has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteActivityViewDAO\\:\\:\\$TableAction has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteActivityViewDAO\\:\\:\\$TableArgs has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\$property of method OnsiteActivityViewMap\\:\\:SetFetchingStrategy\\(\\) has invalid type unknown\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$AuthorizeSignedTime has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$CreateDate has unknown class timestamp as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$FullDocument has unknown class blob as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$PatientSignedTime has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsiteDocumentDAO\\:\\:\\$ReviewDate has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\$property of method OnsiteDocumentMap\\:\\:SetFetchingStrategy\\(\\) has invalid type unknown\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$ActionTakenTime has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Checksum has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Date has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$Narrative has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$TableAction has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property OnsitePortalActivityDAO\\:\\:\\$TableArgs has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\$property of method OnsitePortalActivityMap\\:\\:SetFetchingStrategy\\(\\) has invalid type unknown\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Date has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/PatientDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Dob has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/PatientDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Occupation has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/PatientDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property PatientDAO\\:\\:\\$Regdate has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/PatientDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\$property of method PatientMap\\:\\:SetFetchingStrategy\\(\\) has invalid type unknown\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/PatientMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property UserDAO\\:\\:\\$Info has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property UserDAO\\:\\:\\$Password has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property UserDAO\\:\\:\\$PwdExpirationDate has unknown class date as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property UserDAO\\:\\:\\$PwdHistory1 has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property UserDAO\\:\\:\\$PwdHistory2 has unknown class longtext as its type\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserDAO.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\$property of method UserMap\\:\\:SetFetchingStrategy\\(\\) has invalid type unknown\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserMap.php', -]; $ignoreErrors[] = [ 'message' => '#^Function report_header_2\\(\\) has invalid return type outputs\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/if.alwaysTrue.php b/.phpstan/baseline/if.alwaysTrue.php index 42ddf705206d..687dc8336c12 100644 --- a/.phpstan/baseline/if.alwaysTrue.php +++ b/.phpstan/baseline/if.alwaysTrue.php @@ -343,7 +343,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^If condition is always true\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/_global_config.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php index 2dcdb70e7d20..6b0833def4c1 100644 --- a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php +++ b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php @@ -38203,52 +38203,27 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset string on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset unknown on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteActivityViewMap.php', ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset string on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset unknown on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsiteDocumentMap.php', ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset string on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset unknown on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/OnsitePortalActivityMap.php', ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset string on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/PatientMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset unknown on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/PatientMap.php', ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset string on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserMap.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset unknown on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../portal/patient/libs/Model/DAO/UserMap.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/property.defaultValue.php b/.phpstan/baseline/property.defaultValue.php index 4212409915a9..6029ceb14b4b 100644 --- a/.phpstan/baseline/property.defaultValue.php +++ b/.phpstan/baseline/property.defaultValue.php @@ -16,26 +16,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/smarty_legacy/smarty/Smarty_Legacy.class.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$CONVERT_NULL_TO_EMPTYSTRING \\(Setting\\) does not accept default value of type true\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$DEBUG_MODE \\(set\\) does not accept default value of type false\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$DEFAULT_ACTION \\(default\\) does not accept default value of type string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Static property GlobalConfig\\:\\:\\$TEMPLATE_ENGINE \\(specify\\) does not accept default value of type string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; $ignoreErrors[] = [ 'message' => '#^Property Savant3_Plugin_date\\:\\:\\$default \\(array\\) does not accept default value of type string\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/return.type.php b/.phpstan/baseline/return.type.php index 14a47d815132..06831f4c410a 100644 --- a/.phpstan/baseline/return.type.php +++ b/.phpstan/baseline/return.type.php @@ -2216,11 +2216,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/_global_config.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method GlobalConfig\\:\\:GetDefaultAction\\(\\) should return string but returns default\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../portal/patient/_global_config.php', -]; $ignoreErrors[] = [ 'message' => '#^Method GlobalConfig\\:\\:GetPhreezer\\(\\) should return Phreezer but returns mixed\\.$#', 'count' => 1, diff --git a/.phpstan/fatal-baseline-caps.php b/.phpstan/fatal-baseline-caps.php index 29d55c6c8ea2..540c596a8ae6 100644 --- a/.phpstan/fatal-baseline-caps.php +++ b/.phpstan/fatal-baseline-caps.php @@ -35,7 +35,7 @@ return [ 'all' => [ - 'class.notFound.php' => 229, + 'class.notFound.php' => 184, 'classConstant.notFound.php' => 0, 'constant.notFound.php' => 0, 'function.notFound.php' => 0, diff --git a/portal/patient/_global_config.php b/portal/patient/_global_config.php index abc4db4a346b..0dc73fca8195 100644 --- a/portal/patient/_global_config.php +++ b/portal/patient/_global_config.php @@ -30,38 +30,38 @@ */ class GlobalConfig { - /** @var set to true to send debug info to the browser */ + /** @var bool set to true to send debug info to the browser */ public static $DEBUG_MODE = false; public static $PORTAL = false; - /** @var default action is the controller.method fired when no route is specified */ + /** @var string default action is the controller.method fired when no route is specified */ public static $DEFAULT_ACTION = "Provider.Home"; - /** @var routemap is an array of patterns and routes */ + /** @var array> routemap is an array of patterns and routes */ public static $ROUTE_MAP; - /** @var specify the template render engine (Smarty, Savant, PHP) */ + /** @var string specify the template render engine (Smarty, Savant, PHP) */ public static $TEMPLATE_ENGINE = 'SavantRenderEngine'; - /** @var template path is the physical location of view template files */ + /** @var string template path is the physical location of view template files */ public static $TEMPLATE_PATH; - /** @var template cache path is the physical location where templates can be cached */ + /** @var string template cache path is the physical location where templates can be cached */ public static $TEMPLATE_CACHE_PATH; - /** @var app root is the root directory of the application */ + /** @var string app root is the root directory of the application */ public static $APP_ROOT; - /** @var root url of the application */ + /** @var string root url of the application */ public static $ROOT_URL; - /** @var root url of the application */ + /** @var string root url of the application */ public static $WEB_ROOT; /** @var ConnectionSetting object containing settings for the DB connection **/ public static $CONNECTION_SETTING; - /** @var Setting to true will convert all NULL values to an empty string (set to false with caution!) **/ + /** @var bool setting to true will convert all NULL values to an empty string (set to false with caution!) **/ public static $CONVERT_NULL_TO_EMPTYSTRING = true; /** @var ICache (optional) object for level 2 caching (for example memcached) **/ @@ -211,7 +211,9 @@ function GetRenderEngine() require_once 'verysimple/Phreeze/' . $engine_class . '.php'; } - $this->render_engine = new $engine_class(self::$TEMPLATE_PATH, self::$TEMPLATE_CACHE_PATH); + $engine = new $engine_class(self::$TEMPLATE_PATH, self::$TEMPLATE_CACHE_PATH); + assert($engine instanceof IRenderEngine); + $this->render_engine = $engine; $this->render_engine->assign("ROOT_URL", self::$ROOT_URL); $this->render_engine->assign("PHREEZE_VERSION", Phreezer::$Version); $this->render_engine->assign("PHREEZE_PHAR", Phreezer::PharPath()); diff --git a/portal/patient/fwk/libs/verysimple/Phreeze/IDaoMap2.php b/portal/patient/fwk/libs/verysimple/Phreeze/IDaoMap2.php index b63925884b86..a6130d6f855b 100644 --- a/portal/patient/fwk/libs/verysimple/Phreeze/IDaoMap2.php +++ b/portal/patient/fwk/libs/verysimple/Phreeze/IDaoMap2.php @@ -31,7 +31,7 @@ static function AddMap($property, FieldMap $map); /** * Change the fetching strategy for a KeyMap * - * @param unknown $property + * @param string $property * @param int $loadType * (KM_LOAD_LAZY | KM_LOAD_INNER | KM_LOAD_EAGER) */ diff --git a/portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php b/portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php index 0aa9b8ebc332..57fd18b80f60 100644 --- a/portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php +++ b/portal/patient/libs/Model/DAO/OnsiteActivityViewDAO.php @@ -33,7 +33,7 @@ class OnsiteActivityViewDAO extends Phreezable /** @var int */ public $Id; - /** @var date */ + /** @var string */ public $Date; /** @var int */ @@ -54,22 +54,22 @@ class OnsiteActivityViewDAO extends Phreezable /** @var string */ public $Status; - /** @var longtext */ + /** @var string */ public $Narrative; - /** @var longtext */ + /** @var string */ public $TableAction; - /** @var longtext */ + /** @var string */ public $TableArgs; /** @var int */ public $ActionUser; - /** @var date */ + /** @var string */ public $ActionTakenTime; - /** @var longtext */ + /** @var string */ public $Checksum; /** @var string */ @@ -84,7 +84,7 @@ class OnsiteActivityViewDAO extends Phreezable /** @var string */ public $Mname; - /** @var date */ + /** @var string */ public $Dob; /** @var string */ diff --git a/portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php b/portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php index ec552774aae7..0b195e10a1d0 100644 --- a/portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php +++ b/portal/patient/libs/Model/DAO/OnsiteDocumentDAO.php @@ -45,7 +45,7 @@ class OnsiteDocumentDAO extends Phreezable /** @var int */ public $Encounter; - /** @var timestamp */ + /** @var string */ public $CreateDate; /** @var string */ @@ -54,10 +54,10 @@ class OnsiteDocumentDAO extends Phreezable /** @var int */ public $PatientSignedStatus; - /** @var date */ + /** @var string */ public $PatientSignedTime; - /** @var date */ + /** @var string */ public $AuthorizeSignedTime; /** @var int */ @@ -66,7 +66,7 @@ class OnsiteDocumentDAO extends Phreezable /** @var string */ public $AuthorizingSignator; - /** @var date */ + /** @var string */ public $ReviewDate; /** @var string */ @@ -78,7 +78,7 @@ class OnsiteDocumentDAO extends Phreezable /** @var string */ public $PatientSignature; - /** @var blob */ + /** @var string */ public $FullDocument; /** @var string */ diff --git a/portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php b/portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php index 0401318548bf..eaa1936b7eba 100644 --- a/portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php +++ b/portal/patient/libs/Model/DAO/OnsitePortalActivityDAO.php @@ -33,7 +33,7 @@ class OnsitePortalActivityDAO extends Phreezable /** @var int */ public $Id; - /** @var date */ + /** @var string */ public $Date; /** @var int */ @@ -54,21 +54,21 @@ class OnsitePortalActivityDAO extends Phreezable /** @var string */ public $Status; - /** @var longtext */ + /** @var string */ public $Narrative; - /** @var longtext */ + /** @var string */ public $TableAction; - /** @var longtext */ + /** @var string */ public $TableArgs; /** @var int */ public $ActionUser; - /** @var date */ + /** @var string */ public $ActionTakenTime; - /** @var longtext */ + /** @var string */ public $Checksum; } diff --git a/portal/patient/libs/Model/DAO/PatientDAO.php b/portal/patient/libs/Model/DAO/PatientDAO.php index 43ae8c143640..354feb209a4b 100644 --- a/portal/patient/libs/Model/DAO/PatientDAO.php +++ b/portal/patient/libs/Model/DAO/PatientDAO.php @@ -51,7 +51,7 @@ class PatientDAO extends Phreezable /** @var string */ public $Mname; - /** @var date */ + /** @var string */ public $Dob; /** @var string */ @@ -75,7 +75,7 @@ class PatientDAO extends Phreezable /** @var string */ public $Ss; - /** @var longtext */ + /** @var string */ public $Occupation; /** @var string */ @@ -99,7 +99,7 @@ class PatientDAO extends Phreezable /** @var string */ public $ContactRelationship; - /** @var date */ + /** @var string */ public $Date; /** @var string */ @@ -162,7 +162,7 @@ class PatientDAO extends Phreezable /** @var string */ public $HipaaAllowemail; - /** @var date */ + /** @var string */ public $Regdate; /** @var string */ diff --git a/portal/patient/libs/Model/DAO/UserDAO.php b/portal/patient/libs/Model/DAO/UserDAO.php index b12d09bbf680..8dd16436b395 100644 --- a/portal/patient/libs/Model/DAO/UserDAO.php +++ b/portal/patient/libs/Model/DAO/UserDAO.php @@ -33,11 +33,11 @@ class UserDAO extends Phreezable public $Id; /** @var string */ public $Username; -/** @var longtext */ +/** @var string */ public $Password; /** @var int */ public $Authorized; -/** @var longtext */ +/** @var string */ public $Info; /** @var int */ public $Source; @@ -123,11 +123,11 @@ class UserDAO extends Phreezable public $Calendar; /** @var string */ public $AbookType; -/** @var date */ +/** @var string */ public $PwdExpirationDate; -/** @var longtext */ +/** @var string */ public $PwdHistory1; -/** @var longtext */ +/** @var string */ public $PwdHistory2; /** @var string */ public $DefaultWarehouse; From ed8b42668e0a1f8580f1bcdf9cc56a74a5e9c428 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 13:23:14 -0400 Subject: [PATCH 18/82] fix(encounter): handle missing row and null uuid in encounter view form (#11883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `C_EncounterVisitForm::render()` calls `UuidRegistry::uuidToString($result['uuid'])` immediately after `sqlQuery("SELECT * FROM form_encounter WHERE id = ?", [$id])` without checking whether the row was found, or whether the `uuid` column was populated. When the request carries a stale or invalid encounter id (e.g. a deleted encounter via a bookmarked URL), or the row exists but predates uuid backfill, `$result['uuid']` is `null` and `Ramsey\Uuid\Uuid::fromBytes(null)` throws: ``` PHP Notice: TypeError: Ramsey\Uuid\Uuid::fromBytes(): Argument #1 ($bytes) must be of type string, null given, called in src/Common/Uuid/UuidRegistry.php on line 287 Stack trace: #0 src/Common/Uuid/UuidRegistry.php(287): Ramsey\Uuid\Uuid::fromBytes(NULL) #1 interface/forms/newpatient/C_EncounterVisitForm.class.php(504): UuidRegistry::uuidToString(NULL) #2 interface/forms/newpatient/common.php(43): C_EncounterVisitForm->render(2) ``` This change: - Throws `RuntimeException` when no row matches, instead of silently continuing with `$encounter = false` and crashing several lines later in an unrelated stack frame. - Passes `''` through to `$encounter['uuid']` when the row exists but `uuid` is `null` (the comment on the original line notes this field is set so the array JSON-encodes — empty string preserves that intent). - Drops the `offsetAccess.nonOffsetAccessible` baseline entry that covered the old unsafe access. ## Test plan - [ ] View an existing encounter — renders normally - [ ] Visit `view_form.php` with a deleted/invalid encounter id — surfaces the explicit "Encounter not found" error rather than a `fromBytes(null)` TypeError - [ ] PHPStan passes (`composer phpstan`) Assisted-by: Claude Code --- .phpstan/baseline/offsetAccess.nonOffsetAccessible.php | 5 ----- interface/forms/newpatient/C_EncounterVisitForm.class.php | 7 ++++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php index 6b0833def4c1..28ec68ae1dae 100644 --- a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php +++ b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php @@ -8596,11 +8596,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../interface/forms/newpatient/C_EncounterVisitForm.class.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'uuid\' on array\\|false\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/forms/newpatient/C_EncounterVisitForm.class.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset 0 on mixed\\.$#', 'count' => 1, diff --git a/interface/forms/newpatient/C_EncounterVisitForm.class.php b/interface/forms/newpatient/C_EncounterVisitForm.class.php index 8821dd2d81dc..6a1103b06086 100644 --- a/interface/forms/newpatient/C_EncounterVisitForm.class.php +++ b/interface/forms/newpatient/C_EncounterVisitForm.class.php @@ -507,9 +507,14 @@ public function render($pid) if ($viewmode) { $id = $_REQUEST['id'] ?? ''; $result = sqlQuery("SELECT * FROM form_encounter WHERE id = ?", [$id]); + if (!is_array($result)) { + throw new \RuntimeException('Encounter not found for id ' . var_export($id, true)); + } $encounter = $result; // it won't encode in the JSON if we don't convert this. - $encounter['uuid'] = UuidRegistry::uuidToString($result['uuid']); + $encounter['uuid'] = $result['uuid'] !== null + ? UuidRegistry::uuidToString($result['uuid']) + : ''; $encounter_followup_id = $encounter['parent_encounter_id'] ?? null; if ($encounter_followup_id) { $q = "SELECT fe.date as date, fe.encounter as encounter FROM form_encounter AS fe " . From 1d8b8938d627fe087bf755bec7a32774e30f8723 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Tue, 28 Apr 2026 10:35:47 -0700 Subject: [PATCH 19/82] fix(db): Log all "helpfuldie" sql errors (#11864) #### Short description of what this changes or resolves: The older-style `HelpfulDie` does not particularly live up to its name. Fixing it the right way is removal in favor of everything using exceptions, which is an ongoing but massive task. Debugging is overly complex. This logs the function and full stack trace any time the legacy global-namespace SQL functions catch/suppress/crash any sort of query failure. Yes, `HelpfulDie` makes an attempt to do this, but it's rather limited in how it works and doesn't go through the SystemLogger. I was hoping this would help with diagnosing some Inferno test issues. It doesn't seem to, but I think it's still a worthwhile addition. #### Changes proposed in this pull request: Add an error-level log to all of the `catch` paths inside the older SQL functions, going to the system logger. #### Was an AI assistant used? Yes / No No --- library/sql.inc.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/library/sql.inc.php b/library/sql.inc.php index f864ae13ae6a..b24d85d589c8 100644 --- a/library/sql.inc.php +++ b/library/sql.inc.php @@ -98,6 +98,7 @@ function sqlStatement($statement, $binds = false) try { return QueryUtils::sqlStatementThrowException($statement, $binds, noLog: false); } catch (SqlQueryException $e) { + ServiceContainer::getLogger()->error('{func} error', ['func' => __FUNCTION__, 'exception' => $e]); HelpfulDie("query failed: $statement", $e->sqlError); } } @@ -159,6 +160,7 @@ function sqlStatementNoLog($statement, $binds = false, $throw_exception_on_error if ($throw_exception_on_error) { throw $e; } + ServiceContainer::getLogger()->error('{func} error', ['func' => __FUNCTION__, 'exception' => $e]); HelpfulDie("query failed: $statement", $e->sqlError); } } @@ -241,6 +243,7 @@ function sqlInsert($statement, $binds = false) try { return QueryUtils::sqlInsert($statement, $binds); } catch (SqlQueryException $e) { + ServiceContainer::getLogger()->error('{func} error', ['func' => __FUNCTION__, 'exception' => $e]); HelpfulDie("insert failed: $statement", $e->sqlError); } } @@ -261,6 +264,7 @@ function sqlQuery($statement, $binds = false) try { return QueryUtils::querySingleRow($statement, $binds ?: []); } catch (SqlQueryException $e) { + ServiceContainer::getLogger()->error('{func} error', ['func' => __FUNCTION__, 'exception' => $e]); HelpfulDie("query failed: $statement", $e->sqlError); } } @@ -291,6 +295,7 @@ function sqlQueryNoLog($statement, $binds = false, $throw_exception_on_error = f if ($throw_exception_on_error) { throw $e; } + ServiceContainer::getLogger()->error('{func} error', ['func' => __FUNCTION__, 'exception' => $e]); HelpfulDie("query failed: $statement", $e->sqlError); } } @@ -332,6 +337,7 @@ function sqlInsertClean_audit($statement, $binds = false): void try { QueryUtils::sqlStatementThrowException($statement, $binds, noLog: true); } catch (SqlQueryException $e) { + ServiceContainer::getLogger()->error('{func} error', ['func' => __FUNCTION__, 'exception' => $e]); HelpfulDie("insert failed: $statement", $e->sqlError); } } @@ -434,6 +440,7 @@ function sqlQ($statement, $binds = false) try { return QueryUtils::sqlStatementThrowException($statement, $binds); } catch (SqlQueryException $e) { + ServiceContainer::getLogger()->error('{func} error', ['func' => __FUNCTION__, 'exception' => $e]); HelpfulDie("query failed: $statement", $e->sqlError); } } From 550881b00091b215297f1729a5cdf7c32b0638e4 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 13:54:20 -0400 Subject: [PATCH 20/82] refactor(edihistory): lift edih_x12_file to OpenEMR\Billing\EdiHistory\X12File (#11879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Refs #11792. Lifts the procedural `edih_x12_file` class from `library/edihistory/edih_x12file_class.php` into the modern PSR-4 namespace `OpenEMR\Billing\EdiHistory\X12File`, leaving a `class_alias` shim so all existing legacy callers keep working. This PR is **behavior-preserving** — no type tightening beyond what the lift requires. Future PRs will tighten types and drain more baseline entries on the lifted class. ## Changes - Moves `edih_x12_file` class body into `src/Billing/EdiHistory/X12File.php` - Replaces `library/edihistory/edih_x12file_class.php` with a `class_alias` shim - Adds `.phpstan/phpstan_legacy_aliases.php` bootstrap so PHPStan resolves the legacy `edih_x12_file` symbol (it does not follow runtime `class_alias`) - Adds fixture-based isolated tests under `tests/Tests/Isolated/Billing/EdiHistory/X12FileIsolatedTest.php` (24 tests covering happy path, scan/delimiter/type/envelope error paths, no-mk_segs path, and unknown-GS warning) - Removes 81 bare `//` line comments inherited from the original procedural class - Regenerates the PHPStan baseline (fatal-cap counts unchanged) ## Test plan - [x] `composer phpunit-isolated` passes - [x] `composer phpstan` passes - [x] CI green --- .phpstan/baseline/argument.type.php | 282 +-- .phpstan/baseline/assignOp.invalid.php | 10 +- .phpstan/baseline/binaryOp.invalid.php | 280 +-- .../baseline/booleanAnd.leftAlwaysTrue.php | 10 +- .phpstan/baseline/cast.string.php | 10 +- .phpstan/baseline/empty.notAllowed.php | 10 +- .phpstan/baseline/foreach.nonIterable.php | 10 +- .../baseline/function.alreadyNarrowedType.php | 20 +- .phpstan/baseline/isset.variable.php | 10 +- .../baseline/missingType.iterableValue.php | 70 +- .phpstan/baseline/missingType.parameter.php | 100 +- .phpstan/baseline/missingType.property.php | 160 +- .phpstan/baseline/missingType.return.php | 140 +- .../baseline/offsetAccess.invalidOffset.php | 10 +- .../offsetAccess.nonOffsetAccessible.php | 200 +- .phpstan/baseline/offsetAccess.notFound.php | 40 +- .phpstan/baseline/parameter.defaultValue.php | 10 +- .phpstan/baseline/phpDoc.parseError.php | 10 +- .phpstan/baseline/postInc.type.php | 10 +- .phpstan/baseline/return.type.php | 30 +- .phpstan/baseline/return.void.php | 8 +- .phpstan/baseline/variable.undefined.php | 150 +- .phpstan/phpstan_legacy_aliases.php | 25 + library/edihistory/edih_x12file_class.php | 1619 +---------------- phpstan.neon.dist | 1 + src/Billing/EdiHistory/X12File.php | 1550 ++++++++++++++++ .../EdiHistory/X12FileIsolatedTest.php | 331 ++++ 27 files changed, 2712 insertions(+), 2394 deletions(-) create mode 100644 .phpstan/phpstan_legacy_aliases.php create mode 100644 src/Billing/EdiHistory/X12File.php create mode 100644 tests/Tests/Isolated/Billing/EdiHistory/X12FileIsolatedTest.php diff --git a/.phpstan/baseline/argument.type.php b/.phpstan/baseline/argument.type.php index f342d0324d0e..03a60e4bc491 100644 --- a/.phpstan/baseline/argument.type.php +++ b/.phpstan/baseline/argument.type.php @@ -24532,7 +24532,7 @@ 'path' => __DIR__ . '/../../library/edihistory/edih_io.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$clm01 of method edih_x12_file\\:\\:edih_x12_transaction\\(\\) expects string, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$clm01 of method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_transaction\\(\\) expects string, mixed given\\.$#', 'count' => 2, 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', ]; @@ -24716,146 +24716,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$array of function array_slice expects array, mixed given\\.$#', - 'count' => 10, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$array of function array_unique expects an array of values castable to string, list given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$array of function end expects array\\|object, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$array of function key expects array\\|object, mixed given\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$array of function reset expects array\\|object, mixed given\\.$#', - 'count' => 7, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$file_text of method edih_x12_file\\:\\:edih_x12_segments\\(\\) expects string, string\\|false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$file_text of method edih_x12_file\\:\\:edih_x12_type\\(\\) expects string, string\\|false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$filename of function is_file expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$filename of function is_readable expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$haystack of function str_starts_with expects string, string\\|false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$separator of function explode expects non\\-empty\\-string, mixed given\\.$#', - 'count' => 19, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<0, max\\> given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<1, max\\> given\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<\\-1, max\\> given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 12, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function text expects string, string\\|false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#', - 'count' => 14, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$value of function strval expects bool\\|float\\|GMP\\|int\\|resource\\|string\\|null, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$array of function implode expects array\\, list given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$characters of function trim expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$haystack of function array_search expects array, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$offset of function array_slice expects int, mixed given\\.$#', - 'count' => 10, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$length of function array_slice expects int\\|null, mixed given\\.$#', - 'count' => 8, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$offset of function strpos expects int, int\\<0, max\\>\\|false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$offset of function strpos expects int, int\\|false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$separator of function explode expects non\\-empty\\-string, mixed given\\.$#', 'count' => 13, @@ -29836,6 +29696,146 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$array of function array_slice expects array, mixed given\\.$#', + 'count' => 10, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$array of function array_unique expects an array of values castable to string, list given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$array of function end expects array\\|object, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$array of function key expects array\\|object, mixed given\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$array of function reset expects array\\|object, mixed given\\.$#', + 'count' => 7, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$file_text of method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_segments\\(\\) expects string, string\\|false given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$file_text of method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_type\\(\\) expects string, string\\|false given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$filename of function is_file expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$filename of function is_readable expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$haystack of function str_starts_with expects string, string\\|false given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$separator of function explode expects non\\-empty\\-string, mixed given\\.$#', + 'count' => 19, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<0, max\\> given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<1, max\\> given\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<\\-1, max\\> given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', + 'count' => 12, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function text expects string, string\\|false given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#', + 'count' => 14, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$value of function strval expects bool\\|float\\|GMP\\|int\\|resource\\|string\\|null, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\$array of function implode expects array\\, list given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\$characters of function trim expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\$haystack of function array_search expects array, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\$offset of function array_slice expects int, mixed given\\.$#', + 'count' => 10, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#3 \\$length of function array_slice expects int\\|null, mixed given\\.$#', + 'count' => 8, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#3 \\$offset of function strpos expects int, int\\<0, max\\>\\|false given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#3 \\$offset of function strpos expects int, int\\|false given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$line of method OpenEMR\\\\Billing\\\\Hcfa1500\\:\\:putHcfa\\(\\) expects int, mixed given\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/assignOp.invalid.php b/.phpstan/baseline/assignOp.invalid.php index d1955b8b4f79..63d89fa6a92e 100644 --- a/.phpstan/baseline/assignOp.invalid.php +++ b/.phpstan/baseline/assignOp.invalid.php @@ -1641,11 +1641,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\+\\=" between mixed and string results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\.\\=" between mixed and non\\-falsy\\-string results in an error\\.$#', 'count' => 3, @@ -2116,6 +2111,11 @@ 'count' => 30, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\+\\=" between mixed and string results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\+\\=" between \\(float\\|int\\) and mixed results in an error\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/binaryOp.invalid.php b/.phpstan/baseline/binaryOp.invalid.php index 7ee3f0edf6ee..d9493fe80e96 100644 --- a/.phpstan/baseline/binaryOp.invalid.php +++ b/.phpstan/baseline/binaryOp.invalid.php @@ -11671,146 +11671,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\+" between int\\<0, max\\> and mixed results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\+" between mixed and 1 results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\+" between mixed and 2 results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\-" between \\(float\\|int\\) and mixed results in an error\\.$#', - 'count' => 6, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\-" between mixed and 1 results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\-" between mixed and numeric\\-string results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'/GS\\\\\\\\\' and mixed results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'BHT\' and mixed results in an error\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'CLM\' and mixed results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'CLP\' and mixed results in an error\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'GE\' and mixed results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'GS\' and mixed results in an error\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'HL\' and mixed results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'IEA\' and mixed results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'ISA\' and mixed results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'LX\' and mixed results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'PLB\' and mixed results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'REF\' and mixed results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'SE\' and mixed results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'ST\' and mixed results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'TA1\' and mixed results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'TRN\' and mixed results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'GE\' results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'ISA\' results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'SE\' results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'ST\' results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and non\\-falsy\\-string results in an error\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between non\\-falsy\\-string and mixed results in an error\\.$#', - 'count' => 23, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\+" between mixed and mixed results in an error\\.$#', 'count' => 1, @@ -14431,6 +14291,146 @@ 'count' => 13, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\+" between int\\<0, max\\> and mixed results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\+" between mixed and 1 results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\+" between mixed and 2 results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\-" between \\(float\\|int\\) and mixed results in an error\\.$#', + 'count' => 6, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\-" between mixed and 1 results in an error\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\-" between mixed and numeric\\-string results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'/GS\\\\\\\\\' and mixed results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'BHT\' and mixed results in an error\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'CLM\' and mixed results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'CLP\' and mixed results in an error\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'GE\' and mixed results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'GS\' and mixed results in an error\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'HL\' and mixed results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'IEA\' and mixed results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'ISA\' and mixed results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'LX\' and mixed results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'PLB\' and mixed results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'REF\' and mixed results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'SE\' and mixed results in an error\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'ST\' and mixed results in an error\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'TA1\' and mixed results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between \'TRN\' and mixed results in an error\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between mixed and \'GE\' results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between mixed and \'ISA\' results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between mixed and \'SE\' results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between mixed and \'ST\' results in an error\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between mixed and non\\-falsy\\-string results in an error\\.$#', + 'count' => 5, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between non\\-falsy\\-string and mixed results in an error\\.$#', + 'count' => 23, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\+" between mixed and 64 results in an error\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php b/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php index 55ca50799ca4..28886954e1c5 100644 --- a/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php +++ b/.phpstan/baseline/booleanAnd.leftAlwaysTrue.php @@ -36,11 +36,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_archive.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Left side of && is always true\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Left side of && is always true\\.$#', 'count' => 1, @@ -66,6 +61,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/fwk/libs/util/parsecsv.lib.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Left side of && is always true\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Left side of && is always true\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/cast.string.php b/.phpstan/baseline/cast.string.php index 10bc39f5518d..737d0e2de81e 100644 --- a/.phpstan/baseline/cast.string.php +++ b/.phpstan/baseline/cast.string.php @@ -2261,11 +2261,6 @@ 'count' => 9, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 92, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', 'count' => 22, @@ -3016,6 +3011,11 @@ 'count' => 20, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Cannot cast mixed to string\\.$#', + 'count' => 92, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', 'count' => 13, diff --git a/.phpstan/baseline/empty.notAllowed.php b/.phpstan/baseline/empty.notAllowed.php index 116d38e6322a..11fe1f644054 100644 --- a/.phpstan/baseline/empty.notAllowed.php +++ b/.phpstan/baseline/empty.notAllowed.php @@ -2881,11 +2881,6 @@ 'count' => 3, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 1, @@ -3561,6 +3556,11 @@ 'count' => 22, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 3, diff --git a/.phpstan/baseline/foreach.nonIterable.php b/.phpstan/baseline/foreach.nonIterable.php index a407718bc5b3..b7d7045ae022 100644 --- a/.phpstan/baseline/foreach.nonIterable.php +++ b/.phpstan/baseline/foreach.nonIterable.php @@ -1841,11 +1841,6 @@ 'count' => 5, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 9, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', 'count' => 1, @@ -2251,6 +2246,11 @@ 'count' => 6, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', + 'count' => 9, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', 'count' => 2, diff --git a/.phpstan/baseline/function.alreadyNarrowedType.php b/.phpstan/baseline/function.alreadyNarrowedType.php index c5a809f78bda..dbdb0544646d 100644 --- a/.phpstan/baseline/function.alreadyNarrowedType.php +++ b/.phpstan/baseline/function.alreadyNarrowedType.php @@ -186,16 +186,6 @@ 'count' => 4, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to function is_array\\(\\) with array will always evaluate to true\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to function is_string\\(\\) with non\\-falsy\\-string will always evaluate to true\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Call to function is_countable\\(\\) with array\\, non\\-empty\\-array\\> will always evaluate to true\\.$#', 'count' => 1, @@ -246,6 +236,16 @@ 'count' => 2, 'path' => __DIR__ . '/../../portal/portal_payment.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Call to function is_array\\(\\) with array will always evaluate to true\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Call to function is_string\\(\\) with non\\-falsy\\-string will always evaluate to true\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Call to function is_object\\(\\) with ADORecordSet will always evaluate to true\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/isset.variable.php b/.phpstan/baseline/isset.variable.php index 2c44b63ece7b..0478128c91a4 100644 --- a/.phpstan/baseline/isset.variable.php +++ b/.phpstan/baseline/isset.variable.php @@ -131,11 +131,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_io.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$ta1_icn in isset\\(\\) always exists and is not nullable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$pdrow in isset\\(\\) always exists and is not nullable\\.$#', 'count' => 1, @@ -196,6 +191,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/fwk/libs/verysimple/HTTP/RequestUtil.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$ta1_icn in isset\\(\\) always exists and is not nullable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Variable \\$code_types in isset\\(\\) is never defined\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.iterableValue.php b/.phpstan/baseline/missingType.iterableValue.php index 2a62d3a9301c..b417a11d7000 100644 --- a/.phpstan/baseline/missingType.iterableValue.php +++ b/.phpstan/baseline/missingType.iterableValue.php @@ -2456,41 +2456,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_get_segment\\(\\) has parameter \\$seg_array with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_get_segment\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_delimiters\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_envelopes\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_segments\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_slice\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_transaction\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Function escape_sql_column_name\\(\\) has parameter \\$s with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -3766,6 +3731,41 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/Claim.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_get_segment\\(\\) has parameter \\$seg_array with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_get_segment\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_delimiters\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_envelopes\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_segments\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_slice\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_transaction\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Method OpenEMR\\\\Billing\\\\MiscBillingOptions\\:\\:generateDateQualifierSelect\\(\\) has parameter \\$obj with no value type specified in iterable type array\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.parameter.php b/.phpstan/baseline/missingType.parameter.php index da95a4444bf0..9f1d9947229b 100644 --- a/.phpstan/baseline/missingType.parameter.php +++ b/.phpstan/baseline/missingType.parameter.php @@ -24376,56 +24376,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:__construct\\(\\) has parameter \\$file_path with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:__construct\\(\\) has parameter \\$mk_segs with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:__construct\\(\\) has parameter \\$text with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_file_text\\(\\) has parameter \\$delimiters with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_file_text\\(\\) has parameter \\$file_text with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_file_text\\(\\) has parameter \\$segments with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_file_text\\(\\) has parameter \\$type with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_envelopes\\(\\) has parameter \\$file_text with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_slice\\(\\) has parameter \\$arg_array with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_slice\\(\\) has parameter \\$file_text with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Function edih_835_accounting\\(\\) has parameter \\$delimiters with no type specified\\.$#', 'count' => 1, @@ -31976,6 +31926,56 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:__construct\\(\\) has parameter \\$file_path with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:__construct\\(\\) has parameter \\$mk_segs with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:__construct\\(\\) has parameter \\$text with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_file_text\\(\\) has parameter \\$delimiters with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_file_text\\(\\) has parameter \\$file_text with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_file_text\\(\\) has parameter \\$segments with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_file_text\\(\\) has parameter \\$type with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_envelopes\\(\\) has parameter \\$file_text with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_slice\\(\\) has parameter \\$arg_array with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_slice\\(\\) has parameter \\$file_text with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Method OpenEMR\\\\Billing\\\\Hcfa1500\\:\\:genHcfa1500Page\\(\\) has parameter \\$encounter with no type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.property.php b/.phpstan/baseline/missingType.property.php index f1ce8d445897..5021d7c8ea5a 100644 --- a/.phpstan/baseline/missingType.property.php +++ b/.phpstan/baseline/missingType.property.php @@ -4951,86 +4951,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/codes/edih_835_code_class.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$constructing has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$delimiters has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$envelopes has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$filename has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$filepath has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$gstype_ar has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$hasGS has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$hasST has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$isx12 has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$length has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$message has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$segments has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$text has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$type has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$valid has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property edih_x12_file\\:\\:\\$version has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Property RestResponse\\:\\:\\$ErrorMessage has no type specified\\.$#', 'count' => 1, @@ -23726,6 +23646,86 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/Claim.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$constructing has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$delimiters has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$envelopes has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$filename has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$filepath has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$gstype_ar has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$hasGS has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$hasST has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$isx12 has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$length has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$message has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$segments has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$text has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$type has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$valid has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:\\$version has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Property OpenEMR\\\\Billing\\\\Hcfa1500\\:\\:\\$hcfa_curr_col has no type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/missingType.return.php b/.phpstan/baseline/missingType.return.php index c22a50ce926c..606542613fcd 100644 --- a/.phpstan/baseline/missingType.return.php +++ b/.phpstan/baseline/missingType.return.php @@ -15916,76 +15916,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_segments.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_delimiters\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_envelopes\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_file_text\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_filename\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_filepath\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_hasGS\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_hasST\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_isx12\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_length\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_segments\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_text\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_type\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_valid\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_version\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Function edih_835_accounting\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -20081,6 +20011,76 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_delimiters\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_envelopes\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_file_text\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_filename\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_filepath\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_hasGS\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_hasST\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_isx12\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_length\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_segments\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_text\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_type\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_valid\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_version\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Method OpenEMR\\\\Billing\\\\HCFAInfo\\:\\:getColumn\\(\\) has no return type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/offsetAccess.invalidOffset.php b/.phpstan/baseline/offsetAccess.invalidOffset.php index 44ec6ca92dd9..9b6034c925d0 100644 --- a/.phpstan/baseline/offsetAccess.invalidOffset.php +++ b/.phpstan/baseline/offsetAccess.invalidOffset.php @@ -721,11 +721,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Possibly invalid array key type mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Possibly invalid array key type mixed\\.$#', 'count' => 2, @@ -871,6 +866,11 @@ 'count' => 8, 'path' => __DIR__ . '/../../src/Billing/Claim.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Possibly invalid array key type mixed\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Possibly invalid array key type mixed\\.$#', 'count' => 23, diff --git a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php index 28ec68ae1dae..aa1bc207acfb 100644 --- a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php +++ b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php @@ -33716,106 +33716,6 @@ 'count' => 9, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access an offset on mixed\\.$#', - 'count' => 64, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'GS\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'ISA\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'ST\' on mixed\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'acct\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'array\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'count\' on mixed\\.$#', - 'count' => 9, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'delimiters\' on mixed\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'e\' on mixed\\.$#', - 'count' => 9, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'gsn\' on mixed\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'icn\' on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'segments\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'start\' on mixed\\.$#', - 'count' => 10, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'stn\' on mixed\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'t\' on mixed\\.$#', - 'count' => 8, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'trace\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'type\' on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset mixed on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset non\\-falsy\\-string on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset string on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'adj\' on mixed\\.$#', 'count' => 8, @@ -40411,6 +40311,106 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access an offset on mixed\\.$#', + 'count' => 64, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'GS\' on mixed\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'ISA\' on mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'ST\' on mixed\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'acct\' on mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'array\' on mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'count\' on mixed\\.$#', + 'count' => 9, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'delimiters\' on mixed\\.$#', + 'count' => 5, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'e\' on mixed\\.$#', + 'count' => 9, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'gsn\' on mixed\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'icn\' on mixed\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'segments\' on mixed\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'start\' on mixed\\.$#', + 'count' => 10, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'stn\' on mixed\\.$#', + 'count' => 5, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'t\' on mixed\\.$#', + 'count' => 8, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'trace\' on mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset \'type\' on mixed\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset mixed on mixed\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset non\\-falsy\\-string on mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Cannot access offset string on mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'provider_qualifier_code\' on mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/offsetAccess.notFound.php b/.phpstan/baseline/offsetAccess.notFound.php index f6fe81674a48..90a8df052cde 100644 --- a/.phpstan/baseline/offsetAccess.notFound.php +++ b/.phpstan/baseline/offsetAccess.notFound.php @@ -566,26 +566,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_csv_parse.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'GS\' might not exist on array\\{ST\\?\\: non\\-empty\\-array\\, non\\-empty\\-array\\{start\\?\\: numeric\\-string, count\\?\\: string, stn\\?\\: string, gsn\\?\\: mixed, icn\\?\\: mixed, type\\?\\: string, trace\\?\\: string, acct\\?\\: list\\, \\.\\.\\.\\}\\>, GS\\?\\: non\\-empty\\-array\\\\>, ISA\\: non\\-empty\\-array\\, gscount\\: string, start\\: numeric\\-string, sender\\: string, receiver\\: string, icn\\: string, date\\: string, version\\: string\\}\\|array\\{count\\: int\\<1, max\\>, gscount\\: string\\}\\>\\}\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'ST\' might not exist on array\\{ST\\?\\: non\\-empty\\-array\\, non\\-empty\\-array\\{start\\?\\: numeric\\-string, count\\?\\: string, stn\\?\\: string, gsn\\?\\: mixed, icn\\?\\: mixed, type\\?\\: string, trace\\?\\: string, acct\\?\\: list\\, \\.\\.\\.\\}\\>, GS\\: non\\-empty\\-array\\\\>, ISA\\?\\: non\\-empty\\-array\\, gscount\\?\\: string, start\\?\\: numeric\\-string, sender\\?\\: string, receiver\\?\\: string, icn\\?\\: string, date\\?\\: string, version\\?\\: string\\}\\>\\}\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'count\' might not exist on array\\{start\\: mixed, count\\?\\: \\(float\\|int\\)\\}\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'icn\' might not exist on array\\{count\\: int\\<1, max\\>, gscount\\: string, start\\: numeric\\-string, sender\\: string, receiver\\: string, icn\\: string, date\\: string, version\\: string\\}\\|array\\{count\\: int\\<1, max\\>, gscount\\: string\\}\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Offset 1 might not exist on \'\'\\|non\\-empty\\-list\\\\.$#', 'count' => 1, @@ -841,6 +821,26 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'GS\' might not exist on array\\{ST\\?\\: non\\-empty\\-array\\, non\\-empty\\-array\\{start\\?\\: numeric\\-string, count\\?\\: string, stn\\?\\: string, gsn\\?\\: mixed, icn\\?\\: mixed, type\\?\\: string, trace\\?\\: string, acct\\?\\: list\\, \\.\\.\\.\\}\\>, GS\\?\\: non\\-empty\\-array\\\\>, ISA\\: non\\-empty\\-array\\, gscount\\: string, start\\: numeric\\-string, sender\\: string, receiver\\: string, icn\\: string, date\\: string, version\\: string\\}\\|array\\{count\\: int\\<1, max\\>, gscount\\: string\\}\\>\\}\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'ST\' might not exist on array\\{ST\\?\\: non\\-empty\\-array\\, non\\-empty\\-array\\{start\\?\\: numeric\\-string, count\\?\\: string, stn\\?\\: string, gsn\\?\\: mixed, icn\\?\\: mixed, type\\?\\: string, trace\\?\\: string, acct\\?\\: list\\, \\.\\.\\.\\}\\>, GS\\: non\\-empty\\-array\\\\>, ISA\\?\\: non\\-empty\\-array\\, gscount\\?\\: string, start\\?\\: numeric\\-string, sender\\?\\: string, receiver\\?\\: string, icn\\?\\: string, date\\?\\: string, version\\?\\: string\\}\\>\\}\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'count\' might not exist on array\\{start\\: mixed, count\\?\\: \\(float\\|int\\)\\}\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'icn\' might not exist on array\\{count\\: int\\<1, max\\>, gscount\\: string, start\\: numeric\\-string, sender\\: string, receiver\\: string, icn\\: string, date\\: string, version\\: string\\}\\|array\\{count\\: int\\<1, max\\>, gscount\\: string\\}\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Offset \'bal\' might not exist on array\\{chg\\: float\\|int, bal\\: float\\|int, code_type\\: mixed, code_value\\: mixed, modifier\\: mixed, code_text\\: mixed, dtl\\?\\: non\\-empty\\-array\\<\' 1000\'\\|\' 1001\', array\\{chg\\: numeric\\-string\\}\\>\\}\\|array\\{chg\\: float\\|int, bal\\?\\: float\\|int, dtl\\?\\: non\\-empty\\-array\\<\' 1000\'\\|\' 1001\', array\\{chg\\: numeric\\-string\\}\\>\\}\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/parameter.defaultValue.php b/.phpstan/baseline/parameter.defaultValue.php index 44edd9e68cbd..d676132e415a 100644 --- a/.phpstan/baseline/parameter.defaultValue.php +++ b/.phpstan/baseline/parameter.defaultValue.php @@ -196,11 +196,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/documents.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Default value of the parameter \\#3 \\$seg_array \\(string\\) of method edih_x12_file\\:\\:edih_get_segment\\(\\) is incompatible with type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Default value of the parameter \\#3 \\$authorized \\(string\\) of function addPnote\\(\\) is incompatible with type int\\.$#', 'count' => 1, @@ -321,6 +316,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../portal/patient/fwk/libs/verysimple/Phreeze/PortalController.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Default value of the parameter \\#3 \\$seg_array \\(string\\) of method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_get_segment\\(\\) is incompatible with type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Default value of the parameter \\#1 \\$fhirResource \\(array\\) of method OpenEMR\\\\Services\\\\FHIR\\\\Condition\\\\FhirConditionEncounterDiagnosisService\\:\\:parseFhirResource\\(\\) is incompatible with type OpenEMR\\\\FHIR\\\\R4\\\\FHIRResource\\\\FHIRDomainResource\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/phpDoc.parseError.php b/.phpstan/baseline/phpDoc.parseError.php index 93783c2ee39e..4bba4b15928e 100644 --- a/.phpstan/baseline/phpDoc.parseError.php +++ b/.phpstan/baseline/phpDoc.parseError.php @@ -1673,11 +1673,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^PHPDoc tag @param has invalid value \\(array note\\: all element values except \'keys\' are strings\\)\\: Unexpected token "note", expected variable at offset 613 on line 14$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^PHPDoc tag @param has invalid value \\(array/string \\$whitelist_items Items used in whitelisting method \\(See function description for details of whitelisting method\\)\\. Standard use is to use a array\\. If use a string, then should be regex expression of allowed @@ -3328,6 +3323,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/BillingProcessor/BillingProcessor.php', ]; +$ignoreErrors[] = [ + 'message' => '#^PHPDoc tag @param has invalid value \\(array note\\: all element values except \'keys\' are strings\\)\\: Unexpected token "note", expected variable at offset 613 on line 14$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^PHPDoc tag @param has invalid value \\(json array \\$options \\["G","P","T"\\], \\["G"\\] or could be legacy string with form "GPT", "G", "012"\\)\\: Unexpected token "array", expected variable at offset 99 on line 4$#', 'count' => 1, diff --git a/.phpstan/baseline/postInc.type.php b/.phpstan/baseline/postInc.type.php index 7831cb438522..4b103ba7c602 100644 --- a/.phpstan/baseline/postInc.type.php +++ b/.phpstan/baseline/postInc.type.php @@ -121,11 +121,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../library/custom_template/add_custombutton.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot use \\+\\+ on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot use \\+\\+ on mixed\\.$#', 'count' => 3, @@ -171,6 +166,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/BillingProcessor/Tasks/GeneratorX12Direct.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Cannot use \\+\\+ on mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Cannot use \\+\\+ on mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/return.type.php b/.phpstan/baseline/return.type.php index 06831f4c410a..19848de7163a 100644 --- a/.phpstan/baseline/return.type.php +++ b/.phpstan/baseline/return.type.php @@ -1781,21 +1781,6 @@ 'count' => 4, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_gs_type\\(\\) should return bool\\|string but returns mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_type\\(\\) should return string but returns false\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:edih_x12_type\\(\\) should return string but returns mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Function oeFormatSDFT\\(\\) should return string but returns mixed\\.$#', 'count' => 1, @@ -2911,6 +2896,21 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/Claim.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_gs_type\\(\\) should return bool\\|string but returns mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_type\\(\\) should return string but returns false\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:edih_x12_type\\(\\) should return string but returns mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Method OpenEMR\\\\Billing\\\\MiscBillingOptions\\:\\:qual_id_to_description\\(\\) should return string\\|null but returns mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/return.void.php b/.phpstan/baseline/return.void.php index 8174dc579527..707d67631d8d 100644 --- a/.phpstan/baseline/return.void.php +++ b/.phpstan/baseline/return.void.php @@ -2,14 +2,14 @@ $ignoreErrors = []; $ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:__construct\\(\\) with return type void returns mixed but should not return anything\\.$#', + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:__construct\\(\\) with return type void returns mixed but should not return anything\\.$#', 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method edih_x12_file\\:\\:__construct\\(\\) with return type void returns true but should not return anything\\.$#', + 'message' => '#^Method OpenEMR\\\\Billing\\\\EdiHistory\\\\X12File\\:\\:__construct\\(\\) with return type void returns true but should not return anything\\.$#', 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', ]; $ignoreErrors[] = [ 'message' => '#^Method OpenEMR\\\\Events\\\\Core\\\\TemplatePageEvent\\:\\:setTwigVariables\\(\\) with return type void returns \\$this\\(OpenEMR\\\\Events\\\\Core\\\\TemplatePageEvent\\) but should not return anything\\.$#', diff --git a/.phpstan/baseline/variable.undefined.php b/.phpstan/baseline/variable.undefined.php index c5009077aceb..0db28961a13a 100644 --- a/.phpstan/baseline/variable.undefined.php +++ b/.phpstan/baseline/variable.undefined.php @@ -13816,81 +13816,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../library/edihistory/edih_uploads.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Undefined variable\\: \\$segment_ar$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Undefined variable\\: \\$st_pos$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$bht_pos might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$dr might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$ds might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$dt might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$env_ar might not be defined\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$gs_ct might not be defined\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$gs_fid might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$gs_start might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$gsn might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$icn might not be defined\\.$#', - 'count' => 10, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$seg_ar might not be defined\\.$#', - 'count' => 8, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$segidx might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$stn might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../library/edihistory/edih_x12file_class.php', -]; $ignoreErrors[] = [ 'message' => '#^Undefined variable\\: \\$de$#', 'count' => 18, @@ -14831,6 +14756,81 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/Billing/EDI270.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Undefined variable\\: \\$segment_ar$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Undefined variable\\: \\$st_pos$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$bht_pos might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$dr might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$ds might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$dt might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$env_ar might not be defined\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$gs_ct might not be defined\\.$#', + 'count' => 5, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$gs_fid might not be defined\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$gs_start might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$gsn might not be defined\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$icn might not be defined\\.$#', + 'count' => 10, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$seg_ar might not be defined\\.$#', + 'count' => 8, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$segidx might not be defined\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$stn might not be defined\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../src/Billing/EdiHistory/X12File.php', +]; $ignoreErrors[] = [ 'message' => '#^Variable \\$MDY might not be defined\\.$#', 'count' => 1, diff --git a/.phpstan/phpstan_legacy_aliases.php b/.phpstan/phpstan_legacy_aliases.php new file mode 100644 index 000000000000..7398ef0f6abe --- /dev/null +++ b/.phpstan/phpstan_legacy_aliases.php @@ -0,0 +1,25 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc. + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +declare(strict_types=1); + +class_alias(\OpenEMR\Billing\EdiHistory\X12File::class, 'edih_x12_file'); diff --git a/library/edihistory/edih_x12file_class.php b/library/edihistory/edih_x12file_class.php index faeb130cdd12..a87c10517cde 100644 --- a/library/edihistory/edih_x12file_class.php +++ b/library/edihistory/edih_x12file_class.php @@ -1,1610 +1,21 @@ - * - * - * @link https://www.open-emr.org - * @author Kevin McCormick - * @package OpenEMR - * @subpackage ediHistory - */ - -/* ********* project notes ================= - * determine GET and POST array elements - * process new files -- type and csv data values - * display tables -- links with GET and POST - * find files -- find transactions - * format display - * - * ========================================== - */ - -/*********** php code here ****************************************************************/ - /** - * Class to read EDI X12 files in healthcare setting - * - * It is assumed that EDI X12 files will have mime-type text/plain; charset=us-ascii - * - * initialize with file path or as empty object, e.g. - * $x12_file = new edih_x12_file(filepath); segment array and envelope array, no file text - * $x12_file = new edih_x12_file(filepath, false); no segment or envelope array, no file text - * $x12_file = new edih_x12_file(filepath, false, true); no segment or envelope array, yes file text - * or - * $x12_file = new edih_x12_file(); empty object, ' _x12_ ' methods available if file text supplied as method argument - * - * The properties filename, type, version, valid, isx12, hasGS, hasST, and delimiters should be available - * if the valid filepath is provided when creating the object. - * - * @param string $filepath default = '' - * @param bool $mk_segs default = true - * @param bool $text default = false - * @return bool|string true for empty object "ovgis" for validated x12 + * Legacy class-alias shim for edih_x12_file. + * + * The class body was lifted to OpenEMR\Billing\EdiHistory\X12File. This + * file remains so existing procedural callers in library/edihistory/* + * keep resolving the unqualified `edih_x12_file` symbol. + * + * @package OpenEMR + * @link https://www.open-emr.org + * @author Kevin McCormick + * @author Michael A. Smith + * @copyright Copyright (c) 2014 Kevin McCormick Longview, Texas + * @copyright Copyright (c) 2026 OpenCoreEMR Inc. + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 */ -class edih_x12_file -{ - // properties - private $filepath = ''; - private $filename = ''; - private $type = ''; - private $version = ''; - private $text = ''; - private $length = 0; - private $valid = false; - private $isx12 = false; - private $hasGS = false; - private $hasST = false; - private $message = []; - private $delimiters = []; - private $segments = []; - private $envelopes = []; - // - private $constructing = false; - // - private $gstype_ar = ['HB' => '271', 'HS' => '270', 'HR' => '276', 'HN' => '277', - 'HI' => '278', 'HP' => '835', 'FA' => '999', 'HC' => '837']; - // - function __construct($file_path = '', $mk_segs = true, $text = false) - { - // - if ($file_path === '') { - return true; - } - - // - if (is_file($file_path) && is_readable($file_path)) { - $this->filepath = trim($file_path); - $this->filename = basename($this->filepath); - $f_text = file_get_contents($this->filepath); - // - $testval = ($f_text) ? $this->edih_x12_scan($f_text) : ''; - $this->valid = ( strpos($testval, 'v') ) ? true : false; - $this->isx12 = ( strpos($testval, 'i') ) ? true : false; - $this->hasGS = ( strpos($testval, 'g') ) ? true : false; - $this->hasST = ( strpos($testval, 's') ) ? true : false; - // - if ($this->valid) { - $this->constructing = true; - $this->text = ($text) ? $f_text : ''; - $this->length = ($f_text) ? strlen($f_text) : 0; - if ($this->isx12) { - $this->delimiters = $this->edih_x12_delimiters(substr($f_text, 0, 126)); - $this->version = substr($f_text, 84, 5); - if ($mk_segs) { - $this->segments = $this->edih_x12_segments($f_text); - if (is_array($this->segments) && count($this->segments)) { - $this->envelopes = $this->edih_x12_envelopes(); - $this->type = $this->edih_x12_type(); - } else { - $this->message[] = 'edih_x12_file: error in creating segment array ' . text($this->filename) . PHP_EOL; - } - } else { - // read file contents to try and determine x12 type - $this->type = $this->edih_x12_type($f_text); - } - } - } - } else { - // invalid file path - $this->message[] = 'edih_x12_file: invalid file path ' . text($file_path); - } - - $this->constructing = false; - return $this->valid; - } - - /* - * function to support empty object and '_x12_' functions called with supplied file text - * - * @param string $file_text - * @param bool return x12 type - * @param bool return delimiters - * @param bool return segments - * @return array array['filetext'] and maybe ['type'] ['$delimiters'] ['segments'] - */ - private function edih_file_text($file_text, $type = false, $delimiters = false, $segments = false) - { - // - $ret_ar = []; - if (!$file_text || is_string($file_text) == false) { - $this->message[] = 'edih_file_text(): invalid argument'; - return $ret_ar; - } - - // do verifications - $v = $this->edih_x12_scan($file_text); - if (!strpos($v, 's')) { - $this->message[] = 'edih_file_text(): failed scan of file text (' . text($v) . ')'; - return $ret_ar; - } - - // - $this->constructing = true; - // - if ($type) { - $ret_ar['type'] = $this->edih_x12_type($file_text); - } - - if ($delimiters) { - $ret_ar['delimiters'] = $this->edih_x12_delimiters(substr($file_text, 0, 126)); - } - - if ($segments) { - $ret_ar['segments'] = $this->edih_x12_segments($file_text); - } - - // - $this->constructing = false; - // - return $ret_ar; - } - - /** - * functions to return properties - */ - public function edih_filepath() - { - return $this->filepath; - } - public function edih_filename() - { - return $this->filename; - } - public function edih_type() - { - return $this->type; - } - public function edih_version() - { - return $this->version; - } - public function edih_text() - { - return $this->text; - } - public function edih_length() - { - return $this->length; - } - public function edih_valid() - { - return $this->valid; - } - public function edih_isx12() - { - return $this->isx12; - } - public function edih_hasGS() - { - return $this->hasGS; - } - public function edih_hasST() - { - return $this->hasST; - } - public function edih_delimiters() - { - return $this->delimiters; - } - public function edih_segments() - { - return $this->segments; - } - public function edih_envelopes() - { - return $this->envelopes; - } - - /** - * message statements regarding object or from functions - * formatted as html - * - * @return string - */ - public function edih_message() - { - $str_html = '

' . PHP_EOL; - if (count($this->message)) { - foreach ($this->message as $msg) { - $str_html .= text($msg) . '
' . PHP_EOL; - } - - $str_html .= PHP_EOL . '

' . PHP_EOL; - } else { - $str_html = ''; - } - - return $str_html; - } - - - /** - * Numeric type of x12 HC file associated with GS01 code - * - * @param string $gs01 - * @return string|bool - */ - public function edih_gs_type($gs01) - { - $tpky = strtoupper($gs01); - return $this->gstype_ar[$tpky] ?? false; - } - - /** - * Use PHP FileInfo to check mime type and then scan for unwanted characters - * check for Non-basic ASCII character and <%, ?@[\]^_`{|}~ and newline carriage_return - * This function accepts the following mime-type: text/plain; charset=us-ascii - * - * The return string can be 'ovigs' ov - valid, igs - ISA GS ST - * - * @param string $filetext the file contents - * @return string zero length on failure - */ - public function edih_x12_scan($filetext) - { - $hasval = ''; - $ftxt = ( $filetext && is_string($filetext) ) ? trim($filetext) : $filetext; - // possibly $ftxt = trim($filetext, "\x00..\x1F") to remove ASCII control characters - // remove newlines - if (strpos($ftxt, PHP_EOL)) { - $ftxt = str_replace(PHP_EOL, '', $ftxt); - } - - $flen = ( $ftxt && is_string($ftxt) ) ? strlen($ftxt) : 0; - if (!$flen) { - $this->message[] = 'edih_x12_scan: zero length or invalid file text'; - return $hasval; - } - - $de = ''; - $dt = ''; - // use finfo php class - if (class_exists('finfo')) { - $finfo = new finfo(FILEINFO_MIME); - $mimeinfo = $finfo->buffer($ftxt); - if (!str_starts_with($mimeinfo, 'text/plain; charset=us-ascii')) { - $this->message[] = 'edih_x12_scan: ' . text($this->filename) . ' : invalid mime info:
' . text($mimeinfo); - // - return $hasval; - } - } - - // - if (preg_match('/[^\x20-\x7E\x0A\x0D]|(<\?)|(<%)|(message[] = 'edih_x12_scan: suspect characters in file ' . text($this->filename) . '
' . - ' character: ' . text($matches[0][0]) . ' position: ' . text($matches[0][1]); - // - return $hasval; - } - - $hasval = 'ov'; // valid - // check for required segments ISA GS ST; assume segment terminator is last character - if (str_starts_with($ftxt, 'ISA')) { - $hasval = 'ovi'; - $de = substr($ftxt, 3, 1); - $dt = substr($ftxt, -1); - if (strpos($ftxt, $dt . 'GS' . $de, 0)) { - $hasval = 'ovig'; - } - - if (strpos($ftxt, $dt . 'ST' . $de, 0)) { - $hasval = 'ovigs'; - } - } - - return $hasval; - } - - /** - * read the GS segments in file contents to determine x12 type, or, if the - * object was created with a file path and envelopes, from the GS envelope array - * - * @param string $file_text optional contents of an x12 file - * @return string the x12 type, e.g. 837, 835, 277, 999, etc. - */ - public function edih_x12_type($file_text = '') - { - $tpstr = ''; - $tp_tmp = []; - $f_text = ''; - $delims = []; - $delimarg = ''; - $dt = $this->delimiters['t'] ?? ''; - $de = $this->delimiters['e'] ?? ''; - // - if ($file_text) { - // For when '_x12_' function is called with file contents as argument - if (!$this->constructing) { - $vars = $this->edih_file_text($file_text, false, true, false); - $f_text = $file_text; - $dt = $vars['delimiters']['t'] ?? ''; - $de = $vars['delimiters']['e'] ?? ''; - } elseif ($this->text) { - // called in initial construction, delimiters already created if x12 file - $f_text =& $this->text; - if (!$dt) { - $this->message[] = 'edih_x12_type: not x12 file'; - return $tpstr; - } - } else { - // called after file scan, but no segment array exists - $f_text =& $file_text; - if (!$dt) { - $delims = $this->edih_x12_delimiters(substr($f_text, 0, 126)); - $dt = $delims['t'] ?? ''; - $de = $delims['e'] ?? ''; - } - } - - if (!$f_text) { - $this->message[] = 'edih_x12_type: failed scan of file content'; - return $tpstr; - } - } elseif (isset($this->envelopes['GS'])) { - // No argument, so if envelopes exist, take values from there - foreach ($this->envelopes['GS'] as $gs) { - $tp_tmp[] = $gs['type']; - } - } elseif (count($this->segments)) { - // No argument and no envelopes, so scan segments - if (!$de) { - $de = substr((string) reset($this->segments), 3, 1); - } - - foreach ($this->segments as $seg) { - if (strncmp((string) $seg, 'GS' . $de, 3) == 0) { - $gs_ar = explode($de, (string) $seg); - if (array_key_exists($gs_ar[1], $this->gstype_ar)) { - //$tp_tmp[] = $this->gstype_ar[$gs_ar[1]]; - $tp_tmp[] = $gs_ar[1]; - } else { - $tp_tmp[] = $gs_ar[1]; - $this->message[] = 'edih_x12_type: unknown x12 type ' . text($gs_ar[1]); - } - } - } - } else { - $this->message[] = 'edih_x12_type: no content to determine x12 type'; - return $tpstr; - } - - // $f_text has content only if file contents supplied or in text property - if ($f_text) { - // use regular expression instead of strpos($f_text, $dt.'GS'.$de) - $pcrepattern = '/GS\\' . $de . '(?:HB|HS|HR|HI|HN|HP|FA|HC)\\' . $de . '/'; - $pr = preg_match_all($pcrepattern, (string) $f_text, $matches, PREG_OFFSET_CAPTURE); - // - if ($pr && count($matches)) { - foreach ($matches as $m) { - //$gspos1 = $m[0][1]; - $gs_ar1 = explode($de, $m[0][0]); - if (array_key_exists($gs_ar1[1], $this->gstype_ar)) { - //$tp_tmp[] = $this->gstype_ar[$gs_ar1[1]]; - $tp_tmp[] = $gs_ar1[1]; - } else { - $tp_tmp[] = $gs_ar1[1]; - $this->message[] = 'edih_x12_type: unknown x12 type ' . text($gs_ar1[1]); - } - } - } else { - $this->message[] = 'edih_x12_type: did not find GS segment '; - } - - /* **** this replaced by preg_match_all() above ****** - } - // scan GS segments - $gs_str = $dt.'GS'.$de; - $gs_pos = 1; - $gse_pos = 2; - while ($gs_pos) { - $gs_pos = strpos($f_text, $gs_str, $gs_pos); - if ($gs_pos) { - $gsterm = strpos($f_text, $dt, $gs_pos+1); - $gsseg = trim(substr($f_text, $gs_pos+1, $gsterm-$gs_pos-1)); - //$gs_ar = explode($de, substr($f_text, $gs_pos+1, $gsterm-$gs_pos-1) ); - $this->message[] = 'edih_x12_type: '.$gsseg.PHP_EOL; - $gs_ar = explode($de, $gsseg); - if ( array_key_exists($gs_ar[1], $this->gstype_ar) ) { - $tp_tmp[] = $this->gstype_ar[$gs_ar[1]]; - } else { - $tp_tmp[] = $gs_ar[1]; - $this->message[] = 'edih_x12_type: unknown x12 type '.$gs_ar[1]; - } - $gs_pos = $gsterm + 1; - } - } - ******************* */ - } - - // x12 type information collected - if (count($tp_tmp)) { - $tp3 = array_values(array_unique($tp_tmp)); - // mixed should not happen -- concatenated ISA envelopes of different types? - $tpstr = ( count($tp3) > 1 ) ? 'mixed|' . implode("|", $tp3) : $tp3[0]; - //$this->message[] = 'edih_x12_type: ' . $tpstr; - } else { - $this->message[] = 'edih_x12_type: error in identifying type '; - return false; - } - - return $tpstr; - } - - - /** - * Extract x12 delimiters from the ISA segment - * - * There are obviously easier/faster ways of doing this, but we go character by character. - * The value returned is empty on error, otherwise: - *
-     * array('t'=>segment terminator, 'e'=>element delimiter,
-     *       's'=>sub-element delimiter, 'r'=>repetition delimiter)
-     * 
- * - * @param string $isa_str110 first n>=106 characters of x12 file - * @return array array or empty on error - */ - public function edih_x12_delimiters($isa_str110 = '') - { - // - $delim_ar = []; - $isa_str = !$isa_str110 && $this->text ? substr((string) $this->text, 0, 106) : trim($isa_str110); - - $isalen = strlen($isa_str); - if ($isalen >= 106) { - if (!str_starts_with($isa_str, 'ISA')) { - // not the starting characters - $this->message[] = 'edih_x12_delimiters: text does not begin with ISA'; - return $delim_ar; - } - - /* Extract delimiters using the prescribed positions. - * -- problem is possibly mangled files - * $t_ar['e'] = substr($isa_str, 3, 1); - * $t_ar['r'] = substr($isa_str, 82, 1); - * $t_ar['s'] = substr($isa_str, 104, 1); - * $t_ar['t'] = substr($isa_str, 105, 1); - */ - } else { - $this->message[] = 'edih_x12_delimiters: ISA string too short' . PHP_EOL; - return $delim_ar; - } - - $s = ''; - $delim_ct = 0; - $de = substr($isa_str, 3, 1); // ISA* - for ($i = 0; $i < $isalen; $i++) { - if ($isa_str[$i] == $de) { - // element count incremented at end of loop - // repetition separator in version 5010 - if ($delim_ct == 11) { - $dr = substr($s, 1, 1); - } - - if ($delim_ct == 12) { - if (!str_contains($s, '501')) { - $dr = ''; - } - } - - // - if ($delim_ct == 15) { - $ds = substr($isa_str, $i + 1, 1); - $dt = substr($isa_str, $i + 2, 1); - } - - if ($delim_ct == 16) { - break; - } - - $s = $isa_str[$i]; // $elem_delim; - $delim_ct++; - } else { - $s .= $isa_str[$i]; - } - } - - // there are 16 elements in ISA segment - if ($delim_ct < 16) { - // too few elements -- probably did not get delimiters - $this->message[] = "edih_x12_delimiters: too few elements in ISA string"; - return $delim_ar; - } - - // - $delim_ar = ['t' => $dt, 'e' => $de, 's' => $ds, 'r' => $dr]; - // - return $delim_ar; - } - - /** - * Create a multidimensional array of edi envelope info from object segments. - * Useful for slicing and dicing. The ['ST'][$stky]['trace'] value is used only for 835 - * or 999 type files and the ['ST'][$stky]['acct'][i] array will have multiple values - * likely only for 835, 271, and 277 types, because response from a payer will have - * multiple transactions in the ST-SE envelope while OpenEMR probably will place each - * transaction in its own ST-SE envelope for 270 and 837 types. - * - * The ['start'] and ['count'] values are for use in php function array_slice() - * The numeric keys of the segments array begin at 1 and the ['start'] value is one less - * than the actual key because array_slice() offset is zero-based. - * - *
-     * ['ISA'][$icn]=>['start']['count']['sender']['receiver']['icn']['gscount']['date']
-     * ['GS'][$gs_ct]=>['start']['count']['gsn']['icn']['sender']['date']['stcount']['type']
-     * ['ST'][$stky]=>['start']['count']['stn']['gsn']['icn']['type']['trace']['acct']
-     *   ['ST'][$stky]['acct'][i]=>CLM01
-     *   ['ST'][$stky]['bht03'][i]=>BHT03
-     * 
- * - * @return array array as shown above or empty on error - */ - public function edih_x12_envelopes($file_text = '') - { - // produce an array of envelopes and positions - $env_ar = []; - $de = ''; - if ($file_text) { - // presume need for file scan and delimiters - $vars = $this->edih_file_text($file_text, false, true, true); - $segment_ar = $vars['segments'] ?? []; - $de = (isset($vars['delimiters']) ) ? $vars['delimiters']['e'] : ''; - //$segment_ar = $this->edih_x12_segments($file_text); - if (empty($segment_ar) || !$de) { - $this->message[] = 'edih_x12_envelopes: invalid file text'; - return $env_ar; - } - } elseif (count($this->segments)) { - $segment_ar = $this->segments; - if (isset($this->delimiters['e'])) { - $de = $this->delimiters['e']; - } else { - $de = (str_starts_with((string) reset($segment_ar), 'ISA')) ? substr((string) reset($segment_ar), 3, 1) : ''; - } - } else { - $this->message[] = 'edih_x12_envelopes: no text or segments'; - return $env_ar; - } - - if (!$de) { - $this->message[] = 'edih_x12_envelopes: invalid delimiters'; - return $env_ar; - } - - // - // get the segment array bounds - $seg_first = (reset($segment_ar) !== false) ? key($segment_ar) : '1'; - $seg_last = (end($segment_ar) !== false) ? key($segment_ar) : count($segment_ar) + $seg_first; - if (reset($segment_ar) === false) { - $this->message[] = 'edi_x12_envelopes: reset() error in segment array'; - return $env_ar; - } else { - $seg_ct = $seg_last + 1; - } - - // variables - $seg_txt = ''; - $sn = ''; - $st_type = ''; - $st_ct = 0; - $isa_ct = 0; - $iea_ct = 0; - $gs_st_ct = 0; - $trnset_seg_ct = 0; - $st_segs_ct = 0; - $isa_segs_ct = 0; - $chk_trn = false; - $trncd = '2'; - //$id278 = false; - $ta1_icn = ''; - $seg_ar = []; - // the segment IDs we look for - $chk_segs = ['ISA', 'GS' . $de, 'TA1', 'ST' . $de, 'BHT', 'HL' . $de, 'TRN', 'CLP', 'CLM', 'SE' . $de, 'GE' . $de, 'IEA']; - // - for ($i = $seg_first; $i < $seg_ct; $i++) { - // counters - $isa_segs_ct++; - $st_segs_ct++; - // - $seg_text = $segment_ar[$i]; - $sn = substr((string) $seg_text, 0, 4); - // skip over segments that are not envelope boundaries or identifiers - if (!in_array(substr($sn, 0, 3), $chk_segs)) { - continue; - } - - // create the structure array - if (strncmp($sn, 'ISA' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - $icn = trim($seg_ar[13]); - // - $env_ar['ISA'][$icn]['start'] = strval($i - 1); - $env_ar['ISA'][$icn]['sender'] = trim($seg_ar[6]); - $env_ar['ISA'][$icn]['receiver'] = trim($seg_ar[8]); - $env_ar['ISA'][$icn]['icn'] = $icn; - $env_ar['ISA'][$icn]['date'] = trim($seg_ar[9]); // YYMMDD - $env_ar['ISA'][$icn]['version'] = trim($seg_ar[12]); - // - $isa_segs_ct = 1; - $isa_ct++; - continue; - } - - // - if (strncmp($sn, 'GS' . $de, 3) == 0) { - $seg_ar = explode($de, (string) $seg_text); - $gs_start = strval($i - 1); - $gsn = $seg_ar[6]; - // GS06 could be used to id 997/999 response, if truly unique - // cannot index on $gsn due to concatenated ISA envelopes and non-unique - $gs_ct = isset($env_ar['GS']) ? count($env_ar['GS']) : 0; - // - $env_ar['GS'][$gs_ct]['start'] = $gs_start; - $env_ar['GS'][$gs_ct]['gsn'] = $gsn; - $env_ar['GS'][$gs_ct]['icn'] = $icn; - $env_ar['GS'][$gs_ct]['sender'] = trim($seg_ar[2]); - $env_ar['GS'][$gs_ct]['date'] = trim($seg_ar[4]); - $env_ar['GS'][$gs_ct]['srcid'] = ''; - // to verify type of edi transaction - if (array_key_exists($seg_ar[1], $this->gstype_ar)) { - $gs_fid = $this->gstype_ar[$seg_ar[1]]; - $env_ar['GS'][$gs_ct]['type'] = $seg_ar[1]; - } else { - $gs_fid = 'NA'; - $env_ar['GS'][$gs_ct]['type'] = 'NA'; - $this->message[] = 'edih_x12_envelopes: Unknown GS type ' . text($seg_ar[1]); - } - - continue; - } - - // expect 999 TA1 before ST - if (strncmp($sn, 'TA1' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (isset($seg_ar[1]) && $seg_ar[1]) { - $ta1_icn = $seg_ar[1]; - } else { - $this->message[] = 'edih_x12_envelopes: Error in TA1 segment response ICN'; - } - - //TA1*ISA13ICN*ISA09DATE*ISA10TIME*ACKCode*NoteCode~ - continue; - } - - // - if (strncmp($sn, 'ST' . $de, 3) == 0) { - $seg_ar = explode($de, (string) $seg_text); - $stn = $seg_ar[2]; - $st_type = $seg_ar[1]; - $st_start = strval($i); - $st_segs_ct = 1; - $st_ct = isset($env_ar['ST']) ? count($env_ar['ST']) : 0; - // - $env_ar['ST'][$st_ct]['start'] = strval($i - 1); - $env_ar['ST'][$st_ct]['count'] = ''; - $env_ar['ST'][$st_ct]['stn'] = $seg_ar[2]; - $env_ar['ST'][$st_ct]['gsn'] = $gsn; - $env_ar['ST'][$st_ct]['icn'] = $icn; - $env_ar['ST'][$st_ct]['type'] = $seg_ar[1]; - $env_ar['ST'][$st_ct]['trace'] = '0'; - $env_ar['ST'][$st_ct]['acct'] = []; - $env_ar['ST'][$st_ct]['bht03'] = []; - // GS file id FA can be 999 or 997 - if ($gs_fid != $st_type && !str_contains($st_type, '99')) { - $this->message[] = "edih_x12_envelopes: ISA " . text($icn) . ", GS " . text($gsn . " " . $gs_fid) . " ST " . text($stn . " " . $st_type) . " type mismatch" . PHP_EOL; - } - - // - continue; - } - - // - if (strpos('|270|271|276|277|278', $st_type)) { - // - if (strncmp($sn, 'BHT' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (isset($seg_ar[2])) { - $trncd = ($seg_ar[2] == '13') ? '1' : '2'; - // 13 = request, otherwise assume response - } else { - $this->message[] = 'edih_x12_envelopes: missing BHT02 type element'; - } - - if (isset($seg_ar[3]) && $seg_ar[3]) { - $env_ar['ST'][$st_ct]['bht03'][] = $seg_ar[3]; - } else { - $this->message[] = 'edih_x12_envelopes: missing BHT03 identifier'; - } - } - - if (strncmp($sn, 'HL' . $de, 3) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (isset($seg_ar[3]) && $seg_ar[3]) { - $chk_trn = ( strpos('|22|23|PT', $seg_ar[3]) ) ? true : false; - } else { - $this->message[] = 'edih_x12_envelopes: missing HL03 level element'; - } - - continue; - } - - if ($chk_trn && strncmp($sn, 'TRN' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (isset($seg_ar[1]) && $seg_ar[1] == $trncd) { - $env_ar['ST'][$st_ct]['acct'][] = $seg_ar[2] ?? ''; - $chk_trn = false; - } else { - $this->message[] = 'edih_x12_envelopes: missing TRN02 type identifier element'; - } - - continue; - } - } - - // - if ($st_type == '835') { - if (strncmp($sn, 'TRN' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (!isset($seg_ar[2]) || !isset($seg_ar[3])) { - $this->message[] = 'error in 835 TRN segment ' . text($seg_text); - } - - $env_ar['ST'][$st_ct]['trace'] = $seg_ar[2] ?? ""; - // to match OpenEMR billing parse file name - $env_ar['GS'][$gs_ct]['srcid'] = $seg_ar[4] ?? $seg_ar[3] ?? ""; - - // - continue; - } - - if (strncmp($sn, 'CLP' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (isset($seg_ar[1])) { - $env_ar['ST'][$st_ct]['acct'][] = $seg_ar[1]; - } else { - $this->message[] = 'error in 835 CLP segment ' . text($seg_text); - } - - continue; - } - } - - // - if ($st_type == '837') { - if (strncmp($sn, 'BHT' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (isset($seg_ar[3]) && $seg_ar[3]) { - $env_ar['ST'][$st_ct]['bht'][] = $seg_ar[3]; - } else { - $this->message[] = 'edih_x12_envelopes: missing BHT03 identifier'; - } - } - - // - if (strncmp($sn, 'CLM' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - if (isset($seg_ar[1])) { - $env_ar['ST'][$st_ct]['acct'][] = $seg_ar[1]; - } else { - $this->message[] = 'error in 837 CLM segment ' . text($seg_text); - } - - continue; - } - } - - // - if (strncmp($sn, 'SE' . $de, 3) == 0) { - // make sure no lingering toggle - $id278 = false; - $chk_trn = false; - // - $seg_ar = explode($de, (string) $seg_text); - $se_num = $seg_ar[2]; - $env_ar['ST'][$st_ct]['count'] = strval($seg_ar[1]); - // 999 case: expect TA1 before ST, so capture batch icn here - if ($st_type == '999' || $st_type == '997') { - if (isset($ta1_icn) && strlen($ta1_icn)) { - $env_ar['ST'][$st_ct]['trace'] = $ta1_icn; - $ta1_icn = ''; - } - } - - // errors - if ($se_num != $stn) { - $this->message[] = 'edih_x12_envelopes: ST-SE number mismatch ' . text($stn) . ' ' . text($se_num) . ' in ISA ' . text($icn) . PHP_EOL; - } - - if (intval($seg_ar[1]) != $st_segs_ct) { - $this->message[] = 'edih_x12_envelopes: ST-SE segment count mismatch ' . text($st_segs_ct) . ' ' . text($seg_ar[1]) . ' in ISA ' . text($icn) . PHP_EOL; - } - - continue; - } - - // - if (strncmp($sn, 'GE' . $de, 3) == 0) { - $seg_ar = explode($de, (string) $seg_text); - $env_ar['GS'][$gs_ct]['count'] = $i - $gs_start - 1; - $env_ar['GS'][$gs_ct]['stcount'] = trim($seg_ar[1]); // ST count - $gs_st_ct += $seg_ar[1]; - // - if ($seg_ar[2] != $env_ar['GS'][$gs_ct]['gsn']) { - $this->message[] = 'edih_x12_envelopes: GS-GE identifier mismatch' . PHP_EOL; - } - - if ($gs_ct === 0 && ($seg_ar[1] != count($env_ar['ST']))) { - $this->message[] = 'edih_x12_envelopes: GS count of ST mismatch' . PHP_EOL; - } elseif ($gs_st_ct != count($env_ar['ST'])) { - $this->message[] = 'edih_x12_envelopes: GS count of ST mismatch' . PHP_EOL; - } - - continue; - } - - // - if (strncmp($sn, 'IEA' . $de, 4) == 0) { - $seg_ar = explode($de, (string) $seg_text); - $env_ar['ISA'][$icn]['count'] = $isa_segs_ct; - $env_ar['ISA'][$icn]['gscount'] = $seg_ar[1]; - $iea_ct++; - // - if (count($env_ar['GS']) != $seg_ar[1]) { - $this->message[] = 'edih_x12_envelopes: GS count mismatch in ISA ' . text($icn) . PHP_EOL; - $gsct = count($env_ar['GS']); - $this->message[] = 'GS group count: ' . text($gsct) . ' IEA01: ' . text($seg_ar[1]) . ' segment: ' . text($seg_text); - } - - if ($env_ar['ISA'][$icn]['icn'] !== $seg_ar[2]) { - $this->message[] = 'edih_x12_envelopes: ISA-IEA identifier mismatch ISA ' . text($icn) . ' IEA ' . text($seg_ar[2]); - } - - if ($iea_ct == $isa_ct) { - $trnset_seg_ct += $isa_segs_ct; - //if ( $i+1 != $trnset_seg_ct ) { - if ($i != $trnset_seg_ct) { - $this->message[] = 'edih_x12_envelopes: IEA segment count error ' . text($i) . ' : ' . text($trnset_seg_ct); - } - } else { - $this->message[] = 'edih_x12_envelopes: ISA-IEA count mismatch ISA ' . text($isa_ct) . ' IEA ' . text($iea_ct); - } - - continue; - } - } - - // - return $env_ar; - } - - /** - * Parse x12 file contents into array of segments. - * - * @uses edih_x12_delimiters() - * @uses edih_x12_scan() - * - * @param string $file_text - * @return array array['i'] = segment, or empty on error - */ - public function edih_x12_segments($file_text = '') - { - $ar_seg = []; - // do verifications - if ($file_text) { - if (!$this->constructing) { - // need to validate file - $vars = $this->edih_file_text($file_text, false, true, false); - $f_str = $file_text; - $dt = $vars['delimiters']['t'] ?? ''; - } else { - $f_str = $file_text; - if (isset($this->delimiters['t'])) { - $dt = $this->delimiters['t']; - } else { - $delims = $this->edih_x12_delimiters(substr($f_str, 0, 126)); - $dt = $delims['t'] ?? ''; - } - } - } elseif ($this->text) { - $f_str = $this->text; - if (isset($this->delimiters['t'])) { - $dt = $this->delimiters['t']; - } else { - $delims = $this->edih_x12_delimiters(substr((string) $f_str, 0, 126)); - $dt = $delims['t'] ?? ''; - } - } else { - $this->message[] = 'edih_x12_segments: no file text'; - return $ar_seg; - } - - // did we get the segment terminator? - if (!$dt) { - $this->message[] = 'edih_x12_segments: invalid delimiters'; - return $ar_seg; - } - - // OK, now initialize variables - $seg_pos = 0; // position where segment begins - $seg_end = 0; - $seg_ct = 0; - $moresegs = true; - // could test this against simple $segments = explode($dt, $f_str) - while ($moresegs) { - // extract each segment from the file text - $seg_end = strpos((string) $f_str, (string) $dt, $seg_pos); - $seg_text = substr((string) $f_str, $seg_pos, $seg_end - $seg_pos); - $seg_pos = $seg_end + 1; - $moresegs = strpos((string) $f_str, (string) $dt, $seg_pos); - $seg_ct++; - // we trim in case there are line or carriage returns - $ar_seg[$seg_ct] = trim($seg_text); - } - - // - return $ar_seg; - } - - - /** - * extract the segments representing a transaction for CLM01 pt-encounter number - * note: there may be more than one in a file, all matching are returned - * 27x transactions will have unique BHT03 that could be used as the claimid argument - * - * return_array[i] => transaction segments array - * return_array[i][j] => particular segment string - * - * @param string $clm01 837 CLM01 or BHT03 from 277 - * @param string $stn ST number -- optional, limit search to that ST-SE envelope - * @param string $filetext optional file contents - * @return array multidimensional array of segments or empty on failure - */ - public function edih_x12_transaction($clm01, $stn = '', $filetext = '') - { - // - $ret_ar = []; - // - if (!$clm01) { - $this->message[] = 'edih_x12_transaction: invalid argument'; - return $ret_ar; - } - - // - $de = ''; - $tp = ''; - $seg_ar = []; - $env_ar = []; - // select the data to search - if ($filetext && !$this->constructing) { - $vars = $this->edih_file_text($filetext, true, true, true); - $tp = $vars['type'] ?? $tp; - $de = $vars['delimiters']['e'] ?? $de; - $seg_ar = $vars['segments'] ?? $seg_ar; - //$env_ar = $vars['envelopes']; // probably faster without envelopes in this case - } elseif (count($this->segments)) { - // default created object - $seg_ar = $this->segments; - if (count($this->delimiters)) { - $de = $this->delimiters['e']; - } else { - $de = (str_starts_with((string) reset($segment_ar), 'ISA')) ? substr((string) reset($segment_ar), 3, 1) : ''; - } - - $tp = $this->type ?: $this->edih_x12_type(); - $env_ar = ( isset($this->envelopes['ST']) ) ? $this->envelopes : $env_ar; - } elseif ($this->text) { - // object with file text, but no processing - $tp = $this->edih_x12_type(); - $seg_ar = ( $tp ) ? $this->edih_x12_segments() : $seg_ar; - if (count($seg_ar)) { - $de = substr((string) reset($seg_ar), 3, 1); - } - } else { - $this->message[] = 'edih_x12_transaction: invalid search data'; - return $ret_ar; - } - - if (!count($seg_ar)) { - $this->message[] = 'edih_x12_transaction: invalid segments'; - return $ret_ar; - } - - if (!$de) { - $this->message[] = 'edih_x12_transaction: invalid delimiters'; - return $ret_ar; - } - - //array('HB'=>'271', 'HS'=>'270', 'HR'=>'276', 'HI'=>'278', - // 'HN'=>'277', 'HP'=>'835', 'FA'=>'999', 'HC'=>'837'); - if (str_starts_with((string) $tp, 'mixed')) { - $tp = substr((string) $tp, -2); - } - - if (!strpos('|HB|271|HS|270|HR|276|HI|278|HN|277|HP|835|FA|999|HC|837', (string) $tp)) { - $this->message[] = 'edih_x12_transaction: wrong edi type for transaction search ' . text($tp); - return $ret_ar; - } - - $idx = 0; - $is_found = false; - $slice = []; - $srch_ar = []; - $sl_idx = 0; - // there may be several in same ST envelope with the same $clm01, esp. 835 - // we will get each set of relevant transaction segments in foreach() below - if (count($env_ar)) { - foreach ($env_ar['ST'] as $st) { - if (strlen($stn) && $st['stn'] != $stn) { - continue; - } - - if (isset($st['acct']) && count($st['acct'])) { - $ky = array_search($clm01, $st['acct']); - if ($ky !== false) { - $srch_ar[$idx]['array'] = array_slice($seg_ar, $st['start'], $st['count'], true); - $srch_ar[$idx]['start'] = $st['start']; - $srch_ar[$idx]['type'] = $st['type']; - $idx++; - } - } - } - } - - // if not identified in envelope search, use segments - if (!count($srch_ar)) { - $srch_ar[0]['array'] = $seg_ar; - $srch_ar[0]['start'] = 0; // with array_slice() the index is absolute zero base - $srch_ar[0]['type'] = $tp; - } - - // verify we have type - if ($srch_ar[0]['type'] == 'NA' || !$srch_ar[0]['type']) { - $this->edih_message(); - return $ret_ar; - } - - // segments we check - $test_id = ['TRN','CLM','CLP','ST' . $de,'BHT','REF','LX' . $de,'PLB','SE' . $de]; - // - foreach ($srch_ar as $srch) { - $idx = $srch['start'] - 1; // align index to segments array offset - $type = (string)$srch['type']; - $is_found = false; - $idval = ''; - $idlen = 1; - // - foreach ($srch['array'] as $seg) { - $idx++; - // - $test_str = substr((string) $seg, 0, 3); - if (!in_array($test_str, $test_id, true)) { - continue; - } - - // - // the opening ST segment should be in each search array, - // so type and search values can be determined here. - if (strncmp((string) $seg, 'ST' . $de, 3) == 0) { - $stseg = explode($de, (string) $seg); - $type = $type ?: $stseg[1]; - // - $idval = ( strpos('|HN|277|HB|271', $type) ) ? 'TRN' . $de . '2' . $de . $clm01 : ''; - $idval = ( strpos('|HR|276|HS|270', $type) ) ? 'TRN' . $de . '1' . $de . $clm01 : $idval; - $idval = ( strpos('|HI|278', $type) ) ? 'REF' . $de . 'EJ' . $de . $clm01 : $idval; - $idval = ( strpos('|HC|837', $type) ) ? 'CLM' . $de . $clm01 . $de : $idval; - $idval = ( strpos('|HP|835', $type) ) ? 'CLP' . $de . $clm01 . $de : $idval; - $idlen = strlen($idval); - // - continue; - } - - //array('HB'=>'271', 'HS'=>'270', 'HR'=>'276', 'HI'=>'278', - // 'HN'=>'277', 'HP'=>'835', 'FA'=>'999', 'HC'=>'837'); - // these types use the BHT segment to begin transactions - if (strpos('|HI|278|HN|277|HR|276|HB|271|HS|270|HC|837', $type)) { - // - if (strncmp((string) $seg, 'BHT' . $de, 4) === 0) { - $bht_seg = explode($de, (string) $seg); - $bht_pos = $idx; - //$bht_pos = $key; - if ($is_found && isset($slice[$sl_idx]['start'])) { - $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; - //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; - $is_found = false; - $sl_idx++; - } elseif (strcmp($clm01, $bht_seg[3]) === 0) { - // matched by BHT03 identifier - $is_found = true; - $slice[$sl_idx]['start'] = $bht_pos; - } - - continue; - } - - // - if (strncmp((string) $seg, $idval, $idlen) === 0) { - // matched by clm01 identifier (idval) - $is_found = true; - $slice[$sl_idx]['start'] = $bht_pos; - continue; - } - } - - // - if ($type == 'HP' || $type == '835') { - if (strncmp((string) $seg, 'CLP' . $de, 4) === 0) { - if (strncmp((string) $seg, $idval, $idlen) === 0) { - if ($is_found && isset($slice[$sl_idx]['start'])) { - $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; - //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; - $sl_idx++; - } - - $is_found = true; - $slice[$sl_idx]['start'] = $idx; - //$slice[$sl_idx]['start'] = $key; - } else { - if ($is_found && isset($slice[$sl_idx]['start'])) { - $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; - //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; - $is_found = false; - $sl_idx++; - } - } - - continue; - } - - // LX segment is often used to group claim payment information - // we do not capture TS3 or TS2 segments in the transaction - if (strncmp((string) $seg, 'LX' . $de, 3) === 0) { - if ($is_found && isset($slice[$sl_idx]['start'])) { - $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; - //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; - $is_found = false; - $sl_idx++; - } - - continue; - } - - // PLB segment is part of summary/trailer in 835 - // not part of the preceding transaction - if (strncmp((string) $seg, 'PLB' . $de, 4) === 0) { - if ($is_found && isset($slice[$sl_idx]['start'])) { - $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; - //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; - $is_found = false; - $sl_idx++; - } - - continue; - } - } - - // SE will always mark end of transaction segments - if (strncmp((string) $seg, 'SE' . $de, 3) === 0) { - if ($is_found && isset($slice[$sl_idx]['start'])) { - $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; - //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; - $is_found = false; - $sl_idx++; - } - } - } // end foreach($srch['array'] as $seg) - } // end foreach($srch_ar as $srch) - // - if (count($slice)) { - foreach ($slice as $sl) { - $ret_ar[] = array_slice($seg_ar, $sl['start'], $sl['count'], true); - } - } - - // - return $ret_ar; - } - - - /** - * get the segment(s) with a particular ID, such as CLP, NM1, etc. - * return is array - * array[i] => matching segment string - * - * @param string $segmentID such as NM1, CLP, STC, etc. - * @param string $srchStr optional string contained in segment - * @param array $seg_array optional supplied array of segments to search - * @return array - */ - public function edih_get_segment($segmentID, $srchStr = '', $seg_array = '') - { - // - $ret_ar = []; - $seg_ar = []; - $segid = ( strlen($segmentID) ) ? trim($segmentID) : ''; - // - $srch = ( strlen($srchStr) ) ? $srchStr : ''; - - // - if (!$segid) { - $this->message[] = 'edih_get_segment(): missing segment ID'; - return $ret_ar; - } - - // - $de = $this->delimiters['e'] ?? ''; - $dt = $this->delimiters['t'] ?? ''; - // - // segment array from edih_x12_transaction() is two dimension - if (is_array($seg_array) && count($seg_array)) { - if (isset($seg_array[0]) && is_array($seg_array[0])) { - foreach ($seg_array as $ar) { - $seg_ar = array_merge($seg_ar, $ar); - } - } else { - $seg_ar = $seg_array; - } - } elseif (count($this->segments)) { - $seg_ar = $this->segments; - } elseif ($this->text) { - if (!$de) { - $delims = $this->edih_x12_delimiters(substr((string) $this->text, 0, 126)); - $dt = $delims['t'] ?? ''; - $de = $delims['e'] ?? ''; - } - - if (!$de || !$dt) { - $this->message[] = 'edih_get_segment() : unable to get delimiters'; - return $ret_ar; - } - - // - $segsrch = ($segid == 'ISA') ? $segid . $de : $dt . $segid . $de; - $seg_pos = 1; - $see_pos = 2; - while ($seg_pos) { - $seg_pos = strpos((string) $this->text, $segsrch, $seg_pos); - $see_pos = strpos((string) $this->text, (string) $dt, $seg_pos + 1); - if ($seg_pos) { - $segstr = trim(substr((string) $this->text, $seg_pos, $see_pos - $seg_pos), $dt); - if ($srch) { - if (str_contains($segstr, $srch)) { - $ret_ar[] = $segstr; - } - } else { - $ret_ar[] = $segstr; - } - - $seg_pos = $see_pos + 1; - } - } - } - - // - if (count($seg_ar)) { - $cmplen = strlen($segid . $de); - foreach ($seg_ar as $key => $seg) { - if (strncmp((string) $seg, $segid . $de, $cmplen) === 0) { - if ($srch) { - if (str_contains((string) $seg, $srch)) { - $ret_ar[$key] = $seg; - } - } else { - $ret_ar[$key] = $seg; - } - } - } - } else { - $this->message[] = 'edih_get_segment() : no segments or text content available'; - } - - // - return $ret_ar; - } - - - /** - * Get a slice of the segments array - * Supply an array with one or more of the following keys and values: - * - * ['trace'] => trace value from 835(TRN02) or 999(TA101) x12 type - * ['ISA13'] => ISA13 - * ['GS06'] => GS06 (sconsider also 'ISA13') - * ['ST02'] => ST02 (condider also 'ISA13' and 'GS06') - * ['keys'] => true to preserve segment numbering from original file - * - * The return value will be an array of one or more segments. - * The 'search' parameter results in one or more segments containing - * the search string. The - * @param array note: all element values except 'keys' are strings - * @return array - */ - function edih_x12_slice($arg_array, $file_text = '') - { - // - $ret_ar = []; - $f_str = ''; - // see what we have - if (!is_array($arg_array) || !count($arg_array)) { - // debug - $this->message[] = 'edih_x12_slice() invalid array argument'; - return $ret_ar; - } - - // - if ($file_text) { - // need to validate file edih_file_text($file_text, $type=false, $delimiters=false, $segments=false) - $vars = $this->edih_file_text($file_text, true, true, false); - if (is_array($vars) && count($vars)) { - $f_str = $file_text; - $dt = $vars['delimiters']['t'] ?? ''; - $de = $vars['delimiters']['e'] ?? ''; - $ft = $vars['type'] ?? ''; - //$seg_ar = ( isset($vars['segments']) ) ? $vars['segments'] : ''; - //$env_ar = $this->edih_x12_envelopes($f_str); - } else { - $this->message[] = 'edih_x12_slice() error processing file text'; - // debug - //echo $this->edih_message().PHP_EOL; - return $ret_ar; - } - } elseif (count($this->segments) && count($this->envelopes) && count($this->delimiters)) { - $seg_ar = $this->segments; - $env_ar = $this->envelopes; - $dt = $this->delimiters['t']; - $de = $this->delimiters['e']; - $ft = $this->type; - } else { - $this->message[] = 'edih_x12_slice() object missing needed properties'; - // debug - //echo $this->edih_message().PHP_EOL; - return $ret_ar; - } - - // initialize search variables - $trace = ''; - $stn = ''; - $gsn = ''; - $icn = ''; - $prskeys = false; - // - foreach ($arg_array as $key => $val) { - switch ((string)$key) { - case 'trace': - $trace = (string)$val; - break; - case 'ST02': - $stn = (string)$val; - break; - case 'GS06': - $gsn = (string)$val; - break; - case 'ISA13': - $icn = (string)$val; - break; - case 'keys': - $prskeys = (bool)$val; - break; - } - } - - // - if ($trace && !str_contains('|HP|FA', (string) $ft)) { - $this->message[] = 'edih_x12_slice() incorrect type [' . text($ft) . '] for trace'; - return $ret_ar; - } - - // - if ($f_str) { - $srchstr = ''; - if ($icn) { - $icnpos = strpos((string) $f_str, $de . $icn . $de); - if ($icnpos === false) { - // $icn not found - $this->message[] = 'edih_x12_slice() did not find ISA13 ' . text($icn); - // debug - //echo $this->edih_message().PHP_EOL; - return $ret_ar; - } elseif ($icnpos < 106) { - $isapos = 0; - } else { - $isapos = strrpos((string) $f_str, $dt . 'ISA' . $de, ($icnpos - strlen((string) $f_str))) + 1; - } - - $ieapos = strpos((string) $f_str, $de . $icn . $dt, $isapos); - $ieapos = strpos((string) $f_str, (string) $dt, $ieapos) + 1; - $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $isapos + 2) + 1 : 0; - // - $srchstr = substr((string) $f_str, $isapos, $ieapos - $isapos); - } - - if ($gsn) { - $srchstr = $srchstr ?: $f_str; - $gspos = strpos((string) $srchstr, $de . $gsn . $de); - if ($gspos === false) { - // $gsn not found - $this->message[] = 'edih_x12_slice() did not find GS06 ' . text($gsn); - return $ret_ar; - } else { - $gspos = strrpos(substr((string) $srchstr, 0, $gspos), (string) $dt) + 1; - } - - $gepos = strpos((string) $srchstr, $dt . 'GE' . $dt, $gspos); - $gepos = strpos((string) $srchstr, (string) $dt, $gepos + 1) + 1; - $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $gspos + 2) + 1 : 0; - // - $srchstr = substr((string) $srchstr, $gspos, $gepos - $gspos); - } - - if ($stn) { - $srchstr = $srchstr ?: $f_str; - $sttp = $this->gstype_ar[$ft]; - $seg_st = $dt . 'ST' . $de . $sttp . $de . $stn ; - $seg_se = $dt . 'SE' . $de; - // $segpos = 1; - $stpos = strpos((string) $srchstr, $seg_st); - if ($stpos === false) { - // $stn not found - $this->message[] = 'edih_x12_slice() did not find ST02 ' . text($stn); - return $ret_ar; - } else { - $stpos += 1; - } - - $sepos = strpos((string) $srchstr, $seg_se, $stpos); - $sepos = strpos((string) $srchstr, (string) $dt, $sepos + 1); - $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $stpos + 2) + 1 : 0; - // - $srchstr = substr((string) $srchstr, $stpos, $sepos - $stpos); - } - - if ($trace) { - // - $trpos = strpos((string) $f_str, $de . $trace); - if ($trpos === false) { - // $icn not found - $this->message[] = 'edih_x12_slice() did not find trace ' . text($trace); - return $ret_ar; - } - - $sttp = $this->gstype_ar[$ft]; - $seg_st = $dt . 'ST' . $de . $sttp . $de; - $stpos = strrpos((string) $f_str, $seg_st, ($trpos - strlen((string) $f_str))); - $sepos = strpos((string) $f_str, $dt . 'SE' . $de, $stpos); - $sepos = strpos((string) $f_str, (string) $dt, $sepos + 1); - // - $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $st_pos + 2) + 1 : 0; - $srchstr = substr((string) $f_str, $stpos + 1, $sepos - $stpos); - } - - // if we have a match, the $srchstr should have the desired segments - if ($trace || $icn || $gsn || $stn) { - if ($srchstr) { - $seg_ar = explode($dt, $srchstr); - // to keep segment numbers same as original file - foreach ($seg_ar as $seg) { - $ret_ar[$segidx] = $seg; - $segidx++; - } - - return $ret_ar; - } else { - $this->message[] = 'edih_x12_slice() error creating substring'; - return $ret_ar; - } - } - - // file_text not supplied, check for object values - } elseif (!($seg_ar && $env_ar && $dt && $de && $ft)) { - // debug - $this->message[] = 'edih_x12_slice() error is processing file'; - return $ret_ar; - } - - // file_text not supplied, use object values - if ($trace) { - foreach ($env_ar['ST'] as $st) { - if ($st['trace'] == $trace) { - // have to add one to the count to capture the SE segment so html_str has data - // when called from edih_835_payment_html function in edih_835_html.php 4-25-17 SMW - $ret_ar = array_slice($seg_ar, $st['start'], $st['count'] + 1, $prskeys); - break; - } - } - } elseif ($icn && !($stn || $gsn)) { - if (isset($env_ar['ISA'][$icn])) { - $ret_ar = array_slice($seg_ar, $env_ar['ISA'][$icn]['start'], $env_ar['ISA'][$icn]['count'], $prskeys); - } - } elseif ($gsn && !$stn) { - foreach ($env_ar['GS'] as $gs) { - if ($icn) { - if (($gs['icn'] == $icn) && ($gs['gsn'] == $gsn)) { - $ret_ar = array_slice($seg_ar, $gs['start'], $gs['count'], $prskeys); - break; - } - } else { - if ($gs['gsn'] == $gsn) { - $ret_ar = array_slice($seg_ar, $gs['start'], $gs['count'], $prskeys); - break; - } - } - } - } elseif ($stn) { - // ; - foreach ($env_ar['ST'] as $st) { - // - if ($icn) { - if ($gsn) { - if ($st['icn'] == $icn && $st['gsn'] == $gsn && $st['stn'] == $stn) { - $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); - break; - } - } else { - if ($st['icn'] == $icn && $st['stn'] == $stn) { - $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); - break; - } - } - } elseif ($gsn) { - if ($st['gsn'] == $gsn && $st['stn'] == $stn) { - $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); - break; - } - } elseif ($st['stn'] == $stn) { - // - $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); - break; - } - } - } else { - $this->message[] = 'edih_x12_slice() no file text or invalid array argument keys or values'; - } - - // - if (!count($ret_ar)) { - $this->message[] = 'edih_x12_slice() no match'; - } - return $ret_ar; - } +declare(strict_types=1); -// end class edih_x12_file -} +class_alias(\OpenEMR\Billing\EdiHistory\X12File::class, 'edih_x12_file'); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index e6531cb35826..06c5f0096d26 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,6 +5,7 @@ parameters: bootstrapFiles: - .phpstan/phpstan_panther_alias.php - .phpstan/phpstan_include_paths.php + - .phpstan/phpstan_legacy_aliases.php level: 10 # Set tmpDir explicitly so we know what it is tmpDir: tmp-phpstan diff --git a/src/Billing/EdiHistory/X12File.php b/src/Billing/EdiHistory/X12File.php new file mode 100644 index 000000000000..299f90cd72fe --- /dev/null +++ b/src/Billing/EdiHistory/X12File.php @@ -0,0 +1,1550 @@ + + * @copyright Copyright (c) 2014 Kevin McCormick Longview, Texas + * @copyright Copyright (c) 2026 OpenCoreEMR Inc. + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +declare(strict_types=1); + +namespace OpenEMR\Billing\EdiHistory; + +/* + * edih_x12file_class.php + * + * Copyright 2014 Kevin McCormick Longview, Texas + * + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 or later. You should have + * received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * @link https://www.open-emr.org + * @author Kevin McCormick + * @package OpenEMR + * @subpackage ediHistory + */ + +/* ********* project notes ================= + * determine GET and POST array elements + * process new files -- type and csv data values + * display tables -- links with GET and POST + * find files -- find transactions + * format display + * + * ========================================== + */ + +/*********** php code here ****************************************************************/ + +/** + * Class to read EDI X12 files in healthcare setting + * + * It is assumed that EDI X12 files will have mime-type text/plain; charset=us-ascii + * + * initialize with file path or as empty object, e.g. + * $x12_file = new edih_x12_file(filepath); segment array and envelope array, no file text + * $x12_file = new edih_x12_file(filepath, false); no segment or envelope array, no file text + * $x12_file = new edih_x12_file(filepath, false, true); no segment or envelope array, yes file text + * or + * $x12_file = new edih_x12_file(); empty object, ' _x12_ ' methods available if file text supplied as method argument + * + * The properties filename, type, version, valid, isx12, hasGS, hasST, and delimiters should be available + * if the valid filepath is provided when creating the object. + * + * @param string $filepath default = '' + * @param bool $mk_segs default = true + * @param bool $text default = false + * @return bool|string true for empty object "ovgis" for validated x12 + */ +class X12File +{ + // properties + private $filepath = ''; + private $filename = ''; + private $type = ''; + private $version = ''; + private $text = ''; + private $length = 0; + private $valid = false; + private $isx12 = false; + private $hasGS = false; + private $hasST = false; + private $message = []; + private $delimiters = []; + private $segments = []; + private $envelopes = []; + private $constructing = false; + private $gstype_ar = ['HB' => '271', 'HS' => '270', 'HR' => '276', 'HN' => '277', + 'HI' => '278', 'HP' => '835', 'FA' => '999', 'HC' => '837']; + function __construct($file_path = '', $mk_segs = true, $text = false) + { + if ($file_path === '') { + return true; + } + + if (is_file($file_path) && is_readable($file_path)) { + $this->filepath = trim($file_path); + $this->filename = basename($this->filepath); + $f_text = file_get_contents($this->filepath); + $testval = ($f_text) ? $this->edih_x12_scan($f_text) : ''; + $this->valid = ( strpos($testval, 'v') ) ? true : false; + $this->isx12 = ( strpos($testval, 'i') ) ? true : false; + $this->hasGS = ( strpos($testval, 'g') ) ? true : false; + $this->hasST = ( strpos($testval, 's') ) ? true : false; + if ($this->valid) { + $this->constructing = true; + $this->text = ($text) ? $f_text : ''; + $this->length = ($f_text) ? strlen($f_text) : 0; + if ($this->isx12) { + $this->delimiters = $this->edih_x12_delimiters(substr($f_text, 0, 126)); + $this->version = substr($f_text, 84, 5); + if ($mk_segs) { + $this->segments = $this->edih_x12_segments($f_text); + if (is_array($this->segments) && count($this->segments)) { + $this->envelopes = $this->edih_x12_envelopes(); + $this->type = $this->edih_x12_type(); + } else { + $this->message[] = 'edih_x12_file: error in creating segment array ' . \text($this->filename) . PHP_EOL; + } + } else { + // read file contents to try and determine x12 type + $this->type = $this->edih_x12_type($f_text); + } + } + } + } else { + // invalid file path + $this->message[] = 'edih_x12_file: invalid file path ' . \text($file_path); + } + + $this->constructing = false; + return $this->valid; + } + + /* + * function to support empty object and '_x12_' functions called with supplied file text + * + * @param string $file_text + * @param bool return x12 type + * @param bool return delimiters + * @param bool return segments + * @return array array['filetext'] and maybe ['type'] ['$delimiters'] ['segments'] + */ + private function edih_file_text($file_text, $type = false, $delimiters = false, $segments = false) + { + $ret_ar = []; + if (!$file_text || is_string($file_text) == false) { + $this->message[] = 'edih_file_text(): invalid argument'; + return $ret_ar; + } + + // do verifications + $v = $this->edih_x12_scan($file_text); + if (!strpos($v, 's')) { + $this->message[] = 'edih_file_text(): failed scan of file text (' . \text($v) . ')'; + return $ret_ar; + } + + $this->constructing = true; + if ($type) { + $ret_ar['type'] = $this->edih_x12_type($file_text); + } + + if ($delimiters) { + $ret_ar['delimiters'] = $this->edih_x12_delimiters(substr($file_text, 0, 126)); + } + + if ($segments) { + $ret_ar['segments'] = $this->edih_x12_segments($file_text); + } + + $this->constructing = false; + return $ret_ar; + } + + /** + * functions to return properties + */ + public function edih_filepath() + { + return $this->filepath; + } + public function edih_filename() + { + return $this->filename; + } + public function edih_type() + { + return $this->type; + } + public function edih_version() + { + return $this->version; + } + public function edih_text() + { + return $this->text; + } + public function edih_length() + { + return $this->length; + } + public function edih_valid() + { + return $this->valid; + } + public function edih_isx12() + { + return $this->isx12; + } + public function edih_hasGS() + { + return $this->hasGS; + } + public function edih_hasST() + { + return $this->hasST; + } + public function edih_delimiters() + { + return $this->delimiters; + } + public function edih_segments() + { + return $this->segments; + } + public function edih_envelopes() + { + return $this->envelopes; + } + + /** + * message statements regarding object or from functions + * formatted as html + * + * @return string + */ + public function edih_message() + { + $str_html = '

' . PHP_EOL; + if (count($this->message)) { + foreach ($this->message as $msg) { + $str_html .= \text($msg) . '
' . PHP_EOL; + } + + $str_html .= PHP_EOL . '

' . PHP_EOL; + } else { + $str_html = ''; + } + + return $str_html; + } + + + /** + * Numeric type of x12 HC file associated with GS01 code + * + * @param string $gs01 + * @return string|bool + */ + public function edih_gs_type($gs01) + { + $tpky = strtoupper($gs01); + return $this->gstype_ar[$tpky] ?? false; + } + + /** + * Use PHP FileInfo to check mime type and then scan for unwanted characters + * check for Non-basic ASCII character and <%, ?@[\]^_`{|}~ and newline carriage_return + * This function accepts the following mime-type: text/plain; charset=us-ascii + * + * The return string can be 'ovigs' ov - valid, igs - ISA GS ST + * + * @param string $filetext the file contents + * @return string zero length on failure + */ + public function edih_x12_scan($filetext) + { + $hasval = ''; + $ftxt = ( $filetext && is_string($filetext) ) ? trim($filetext) : $filetext; + // possibly $ftxt = trim($filetext, "\x00..\x1F") to remove ASCII control characters + // remove newlines + if (strpos($ftxt, PHP_EOL)) { + $ftxt = str_replace(PHP_EOL, '', $ftxt); + } + + $flen = ( $ftxt && is_string($ftxt) ) ? strlen($ftxt) : 0; + if (!$flen) { + $this->message[] = 'edih_x12_scan: zero length or invalid file text'; + return $hasval; + } + + $de = ''; + $dt = ''; + // use finfo php class + if (class_exists('finfo')) { + $finfo = new \finfo(FILEINFO_MIME); + $mimeinfo = $finfo->buffer($ftxt); + if (!str_starts_with($mimeinfo, 'text/plain; charset=us-ascii')) { + $this->message[] = 'edih_x12_scan: ' . \text($this->filename) . ' : invalid mime info:
' . \text($mimeinfo); + return $hasval; + } + } + + if (preg_match('/[^\x20-\x7E\x0A\x0D]|(<\?)|(<%)|(message[] = 'edih_x12_scan: suspect characters in file ' . \text($this->filename) . '
' . + ' character: ' . \text($matches[0][0]) . ' position: ' . \text($matches[0][1]); + return $hasval; + } + + $hasval = 'ov'; // valid + // check for required segments ISA GS ST; assume segment terminator is last character + if (str_starts_with($ftxt, 'ISA')) { + $hasval = 'ovi'; + $de = substr($ftxt, 3, 1); + $dt = substr($ftxt, -1); + if (strpos($ftxt, $dt . 'GS' . $de, 0)) { + $hasval = 'ovig'; + } + + if (strpos($ftxt, $dt . 'ST' . $de, 0)) { + $hasval = 'ovigs'; + } + } + + return $hasval; + } + + /** + * read the GS segments in file contents to determine x12 type, or, if the + * object was created with a file path and envelopes, from the GS envelope array + * + * @param string $file_text optional contents of an x12 file + * @return string the x12 type, e.g. 837, 835, 277, 999, etc. + */ + public function edih_x12_type($file_text = '') + { + $tpstr = ''; + $tp_tmp = []; + $f_text = ''; + $delims = []; + $delimarg = ''; + $dt = $this->delimiters['t'] ?? ''; + $de = $this->delimiters['e'] ?? ''; + if ($file_text) { + // For when '_x12_' function is called with file contents as argument + if (!$this->constructing) { + $vars = $this->edih_file_text($file_text, false, true, false); + $f_text = $file_text; + $dt = $vars['delimiters']['t'] ?? ''; + $de = $vars['delimiters']['e'] ?? ''; + } elseif ($this->text) { + // called in initial construction, delimiters already created if x12 file + $f_text =& $this->text; + if (!$dt) { + $this->message[] = 'edih_x12_type: not x12 file'; + return $tpstr; + } + } else { + // called after file scan, but no segment array exists + $f_text =& $file_text; + if (!$dt) { + $delims = $this->edih_x12_delimiters(substr($f_text, 0, 126)); + $dt = $delims['t'] ?? ''; + $de = $delims['e'] ?? ''; + } + } + + if (!$f_text) { + $this->message[] = 'edih_x12_type: failed scan of file content'; + return $tpstr; + } + } elseif (isset($this->envelopes['GS'])) { + // No argument, so if envelopes exist, take values from there + foreach ($this->envelopes['GS'] as $gs) { + $tp_tmp[] = $gs['type']; + } + } elseif (count($this->segments)) { + // No argument and no envelopes, so scan segments + if (!$de) { + $de = substr((string) reset($this->segments), 3, 1); + } + + foreach ($this->segments as $seg) { + if (strncmp((string) $seg, 'GS' . $de, 3) == 0) { + $gs_ar = explode($de, (string) $seg); + if (array_key_exists($gs_ar[1], $this->gstype_ar)) { + //$tp_tmp[] = $this->gstype_ar[$gs_ar[1]]; + $tp_tmp[] = $gs_ar[1]; + } else { + $tp_tmp[] = $gs_ar[1]; + $this->message[] = 'edih_x12_type: unknown x12 type ' . \text($gs_ar[1]); + } + } + } + } else { + $this->message[] = 'edih_x12_type: no content to determine x12 type'; + return $tpstr; + } + + // $f_text has content only if file contents supplied or in text property + if ($f_text) { + // use regular expression instead of strpos($f_text, $dt.'GS'.$de) + $pcrepattern = '/GS\\' . $de . '(?:HB|HS|HR|HI|HN|HP|FA|HC)\\' . $de . '/'; + $pr = preg_match_all($pcrepattern, (string) $f_text, $matches, PREG_OFFSET_CAPTURE); + if ($pr && count($matches)) { + foreach ($matches as $m) { + //$gspos1 = $m[0][1]; + $gs_ar1 = explode($de, $m[0][0]); + if (array_key_exists($gs_ar1[1], $this->gstype_ar)) { + //$tp_tmp[] = $this->gstype_ar[$gs_ar1[1]]; + $tp_tmp[] = $gs_ar1[1]; + } else { + $tp_tmp[] = $gs_ar1[1]; + $this->message[] = 'edih_x12_type: unknown x12 type ' . \text($gs_ar1[1]); + } + } + } else { + $this->message[] = 'edih_x12_type: did not find GS segment '; + } + + /* **** this replaced by preg_match_all() above ****** + } + // scan GS segments + $gs_str = $dt.'GS'.$de; + $gs_pos = 1; + $gse_pos = 2; + while ($gs_pos) { + $gs_pos = strpos($f_text, $gs_str, $gs_pos); + if ($gs_pos) { + $gsterm = strpos($f_text, $dt, $gs_pos+1); + $gsseg = trim(substr($f_text, $gs_pos+1, $gsterm-$gs_pos-1)); + //$gs_ar = explode($de, substr($f_text, $gs_pos+1, $gsterm-$gs_pos-1) ); + $this->message[] = 'edih_x12_type: '.$gsseg.PHP_EOL; + $gs_ar = explode($de, $gsseg); + if ( array_key_exists($gs_ar[1], $this->gstype_ar) ) { + $tp_tmp[] = $this->gstype_ar[$gs_ar[1]]; + } else { + $tp_tmp[] = $gs_ar[1]; + $this->message[] = 'edih_x12_type: unknown x12 type '.$gs_ar[1]; + } + $gs_pos = $gsterm + 1; + } + } + ******************* */ + } + + // x12 type information collected + if (count($tp_tmp)) { + $tp3 = array_values(array_unique($tp_tmp)); + // mixed should not happen -- concatenated ISA envelopes of different types? + $tpstr = ( count($tp3) > 1 ) ? 'mixed|' . implode("|", $tp3) : $tp3[0]; + //$this->message[] = 'edih_x12_type: ' . $tpstr; + } else { + $this->message[] = 'edih_x12_type: error in identifying type '; + return false; + } + + return $tpstr; + } + + + /** + * Extract x12 delimiters from the ISA segment + * + * There are obviously easier/faster ways of doing this, but we go character by character. + * The value returned is empty on error, otherwise: + *
+     * array('t'=>segment terminator, 'e'=>element delimiter,
+     *       's'=>sub-element delimiter, 'r'=>repetition delimiter)
+     * 
+ * + * @param string $isa_str110 first n>=106 characters of x12 file + * @return array array or empty on error + */ + public function edih_x12_delimiters($isa_str110 = '') + { + $delim_ar = []; + $isa_str = !$isa_str110 && $this->text ? substr((string) $this->text, 0, 106) : trim($isa_str110); + + $isalen = strlen($isa_str); + if ($isalen >= 106) { + if (!str_starts_with($isa_str, 'ISA')) { + // not the starting characters + $this->message[] = 'edih_x12_delimiters: text does not begin with ISA'; + return $delim_ar; + } + + /* Extract delimiters using the prescribed positions. + * -- problem is possibly mangled files + * $t_ar['e'] = substr($isa_str, 3, 1); + * $t_ar['r'] = substr($isa_str, 82, 1); + * $t_ar['s'] = substr($isa_str, 104, 1); + * $t_ar['t'] = substr($isa_str, 105, 1); + */ + } else { + $this->message[] = 'edih_x12_delimiters: ISA string too short' . PHP_EOL; + return $delim_ar; + } + + $s = ''; + $delim_ct = 0; + $de = substr($isa_str, 3, 1); // ISA* + for ($i = 0; $i < $isalen; $i++) { + if ($isa_str[$i] == $de) { + // element count incremented at end of loop + // repetition separator in version 5010 + if ($delim_ct == 11) { + $dr = substr($s, 1, 1); + } + + if ($delim_ct == 12) { + if (!str_contains($s, '501')) { + $dr = ''; + } + } + + if ($delim_ct == 15) { + $ds = substr($isa_str, $i + 1, 1); + $dt = substr($isa_str, $i + 2, 1); + } + + if ($delim_ct == 16) { + break; + } + + $s = $isa_str[$i]; // $elem_delim; + $delim_ct++; + } else { + $s .= $isa_str[$i]; + } + } + + // there are 16 elements in ISA segment + if ($delim_ct < 16) { + // too few elements -- probably did not get delimiters + $this->message[] = "edih_x12_delimiters: too few elements in ISA string"; + return $delim_ar; + } + + $delim_ar = ['t' => $dt, 'e' => $de, 's' => $ds, 'r' => $dr]; + return $delim_ar; + } + + /** + * Create a multidimensional array of edi envelope info from object segments. + * Useful for slicing and dicing. The ['ST'][$stky]['trace'] value is used only for 835 + * or 999 type files and the ['ST'][$stky]['acct'][i] array will have multiple values + * likely only for 835, 271, and 277 types, because response from a payer will have + * multiple transactions in the ST-SE envelope while OpenEMR probably will place each + * transaction in its own ST-SE envelope for 270 and 837 types. + * + * The ['start'] and ['count'] values are for use in php function array_slice() + * The numeric keys of the segments array begin at 1 and the ['start'] value is one less + * than the actual key because array_slice() offset is zero-based. + * + *
+     * ['ISA'][$icn]=>['start']['count']['sender']['receiver']['icn']['gscount']['date']
+     * ['GS'][$gs_ct]=>['start']['count']['gsn']['icn']['sender']['date']['stcount']['type']
+     * ['ST'][$stky]=>['start']['count']['stn']['gsn']['icn']['type']['trace']['acct']
+     *   ['ST'][$stky]['acct'][i]=>CLM01
+     *   ['ST'][$stky]['bht03'][i]=>BHT03
+     * 
+ * + * @return array array as shown above or empty on error + */ + public function edih_x12_envelopes($file_text = '') + { + // produce an array of envelopes and positions + $env_ar = []; + $de = ''; + if ($file_text) { + // presume need for file scan and delimiters + $vars = $this->edih_file_text($file_text, false, true, true); + $segment_ar = $vars['segments'] ?? []; + $de = (isset($vars['delimiters']) ) ? $vars['delimiters']['e'] : ''; + //$segment_ar = $this->edih_x12_segments($file_text); + if (empty($segment_ar) || !$de) { + $this->message[] = 'edih_x12_envelopes: invalid file text'; + return $env_ar; + } + } elseif (count($this->segments)) { + $segment_ar = $this->segments; + if (isset($this->delimiters['e'])) { + $de = $this->delimiters['e']; + } else { + $de = (str_starts_with((string) reset($segment_ar), 'ISA')) ? substr((string) reset($segment_ar), 3, 1) : ''; + } + } else { + $this->message[] = 'edih_x12_envelopes: no text or segments'; + return $env_ar; + } + + if (!$de) { + $this->message[] = 'edih_x12_envelopes: invalid delimiters'; + return $env_ar; + } + + // get the segment array bounds + $seg_first = (reset($segment_ar) !== false) ? key($segment_ar) : '1'; + $seg_last = (end($segment_ar) !== false) ? key($segment_ar) : count($segment_ar) + $seg_first; + if (reset($segment_ar) === false) { + $this->message[] = 'edi_x12_envelopes: reset() error in segment array'; + return $env_ar; + } else { + $seg_ct = $seg_last + 1; + } + + // variables + $seg_txt = ''; + $sn = ''; + $st_type = ''; + $st_ct = 0; + $isa_ct = 0; + $iea_ct = 0; + $gs_st_ct = 0; + $trnset_seg_ct = 0; + $st_segs_ct = 0; + $isa_segs_ct = 0; + $chk_trn = false; + $trncd = '2'; + //$id278 = false; + $ta1_icn = ''; + $seg_ar = []; + // the segment IDs we look for + $chk_segs = ['ISA', 'GS' . $de, 'TA1', 'ST' . $de, 'BHT', 'HL' . $de, 'TRN', 'CLP', 'CLM', 'SE' . $de, 'GE' . $de, 'IEA']; + for ($i = $seg_first; $i < $seg_ct; $i++) { + // counters + $isa_segs_ct++; + $st_segs_ct++; + $seg_text = $segment_ar[$i]; + $sn = substr((string) $seg_text, 0, 4); + // skip over segments that are not envelope boundaries or identifiers + if (!in_array(substr($sn, 0, 3), $chk_segs)) { + continue; + } + + // create the structure array + if (strncmp($sn, 'ISA' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + $icn = trim($seg_ar[13]); + $env_ar['ISA'][$icn]['start'] = strval($i - 1); + $env_ar['ISA'][$icn]['sender'] = trim($seg_ar[6]); + $env_ar['ISA'][$icn]['receiver'] = trim($seg_ar[8]); + $env_ar['ISA'][$icn]['icn'] = $icn; + $env_ar['ISA'][$icn]['date'] = trim($seg_ar[9]); // YYMMDD + $env_ar['ISA'][$icn]['version'] = trim($seg_ar[12]); + $isa_segs_ct = 1; + $isa_ct++; + continue; + } + + if (strncmp($sn, 'GS' . $de, 3) == 0) { + $seg_ar = explode($de, (string) $seg_text); + $gs_start = strval($i - 1); + $gsn = $seg_ar[6]; + // GS06 could be used to id 997/999 response, if truly unique + // cannot index on $gsn due to concatenated ISA envelopes and non-unique + $gs_ct = isset($env_ar['GS']) ? count($env_ar['GS']) : 0; + $env_ar['GS'][$gs_ct]['start'] = $gs_start; + $env_ar['GS'][$gs_ct]['gsn'] = $gsn; + $env_ar['GS'][$gs_ct]['icn'] = $icn; + $env_ar['GS'][$gs_ct]['sender'] = trim($seg_ar[2]); + $env_ar['GS'][$gs_ct]['date'] = trim($seg_ar[4]); + $env_ar['GS'][$gs_ct]['srcid'] = ''; + // to verify type of edi transaction + if (array_key_exists($seg_ar[1], $this->gstype_ar)) { + $gs_fid = $this->gstype_ar[$seg_ar[1]]; + $env_ar['GS'][$gs_ct]['type'] = $seg_ar[1]; + } else { + $gs_fid = 'NA'; + $env_ar['GS'][$gs_ct]['type'] = 'NA'; + $this->message[] = 'edih_x12_envelopes: Unknown GS type ' . \text($seg_ar[1]); + } + + continue; + } + + // expect 999 TA1 before ST + if (strncmp($sn, 'TA1' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (isset($seg_ar[1]) && $seg_ar[1]) { + $ta1_icn = $seg_ar[1]; + } else { + $this->message[] = 'edih_x12_envelopes: Error in TA1 segment response ICN'; + } + + //TA1*ISA13ICN*ISA09DATE*ISA10TIME*ACKCode*NoteCode~ + continue; + } + + if (strncmp($sn, 'ST' . $de, 3) == 0) { + $seg_ar = explode($de, (string) $seg_text); + $stn = $seg_ar[2]; + $st_type = $seg_ar[1]; + $st_start = strval($i); + $st_segs_ct = 1; + $st_ct = isset($env_ar['ST']) ? count($env_ar['ST']) : 0; + $env_ar['ST'][$st_ct]['start'] = strval($i - 1); + $env_ar['ST'][$st_ct]['count'] = ''; + $env_ar['ST'][$st_ct]['stn'] = $seg_ar[2]; + $env_ar['ST'][$st_ct]['gsn'] = $gsn; + $env_ar['ST'][$st_ct]['icn'] = $icn; + $env_ar['ST'][$st_ct]['type'] = $seg_ar[1]; + $env_ar['ST'][$st_ct]['trace'] = '0'; + $env_ar['ST'][$st_ct]['acct'] = []; + $env_ar['ST'][$st_ct]['bht03'] = []; + // GS file id FA can be 999 or 997 + if ($gs_fid != $st_type && !str_contains($st_type, '99')) { + $this->message[] = "edih_x12_envelopes: ISA " . \text($icn) . ", GS " . \text($gsn . " " . $gs_fid) . " ST " . \text($stn . " " . $st_type) . " type mismatch" . PHP_EOL; + } + + continue; + } + + if (strpos('|270|271|276|277|278', $st_type)) { + if (strncmp($sn, 'BHT' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (isset($seg_ar[2])) { + $trncd = ($seg_ar[2] == '13') ? '1' : '2'; + // 13 = request, otherwise assume response + } else { + $this->message[] = 'edih_x12_envelopes: missing BHT02 type element'; + } + + if (isset($seg_ar[3]) && $seg_ar[3]) { + $env_ar['ST'][$st_ct]['bht03'][] = $seg_ar[3]; + } else { + $this->message[] = 'edih_x12_envelopes: missing BHT03 identifier'; + } + } + + if (strncmp($sn, 'HL' . $de, 3) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (isset($seg_ar[3]) && $seg_ar[3]) { + $chk_trn = ( strpos('|22|23|PT', $seg_ar[3]) ) ? true : false; + } else { + $this->message[] = 'edih_x12_envelopes: missing HL03 level element'; + } + + continue; + } + + if ($chk_trn && strncmp($sn, 'TRN' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (isset($seg_ar[1]) && $seg_ar[1] == $trncd) { + $env_ar['ST'][$st_ct]['acct'][] = $seg_ar[2] ?? ''; + $chk_trn = false; + } else { + $this->message[] = 'edih_x12_envelopes: missing TRN02 type identifier element'; + } + + continue; + } + } + + if ($st_type == '835') { + if (strncmp($sn, 'TRN' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (!isset($seg_ar[2]) || !isset($seg_ar[3])) { + $this->message[] = 'error in 835 TRN segment ' . \text($seg_text); + } + + $env_ar['ST'][$st_ct]['trace'] = $seg_ar[2] ?? ""; + // to match OpenEMR billing parse file name + $env_ar['GS'][$gs_ct]['srcid'] = $seg_ar[4] ?? $seg_ar[3] ?? ""; + + continue; + } + + if (strncmp($sn, 'CLP' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (isset($seg_ar[1])) { + $env_ar['ST'][$st_ct]['acct'][] = $seg_ar[1]; + } else { + $this->message[] = 'error in 835 CLP segment ' . \text($seg_text); + } + + continue; + } + } + + if ($st_type == '837') { + if (strncmp($sn, 'BHT' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (isset($seg_ar[3]) && $seg_ar[3]) { + $env_ar['ST'][$st_ct]['bht'][] = $seg_ar[3]; + } else { + $this->message[] = 'edih_x12_envelopes: missing BHT03 identifier'; + } + } + + if (strncmp($sn, 'CLM' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + if (isset($seg_ar[1])) { + $env_ar['ST'][$st_ct]['acct'][] = $seg_ar[1]; + } else { + $this->message[] = 'error in 837 CLM segment ' . \text($seg_text); + } + + continue; + } + } + + if (strncmp($sn, 'SE' . $de, 3) == 0) { + // make sure no lingering toggle + $id278 = false; + $chk_trn = false; + $seg_ar = explode($de, (string) $seg_text); + $se_num = $seg_ar[2]; + $env_ar['ST'][$st_ct]['count'] = strval($seg_ar[1]); + // 999 case: expect TA1 before ST, so capture batch icn here + if ($st_type == '999' || $st_type == '997') { + if (isset($ta1_icn) && strlen($ta1_icn)) { + $env_ar['ST'][$st_ct]['trace'] = $ta1_icn; + $ta1_icn = ''; + } + } + + // errors + if ($se_num != $stn) { + $this->message[] = 'edih_x12_envelopes: ST-SE number mismatch ' . \text($stn) . ' ' . \text($se_num) . ' in ISA ' . \text($icn) . PHP_EOL; + } + + if (intval($seg_ar[1]) != $st_segs_ct) { + $this->message[] = 'edih_x12_envelopes: ST-SE segment count mismatch ' . \text($st_segs_ct) . ' ' . \text($seg_ar[1]) . ' in ISA ' . \text($icn) . PHP_EOL; + } + + continue; + } + + if (strncmp($sn, 'GE' . $de, 3) == 0) { + $seg_ar = explode($de, (string) $seg_text); + $env_ar['GS'][$gs_ct]['count'] = $i - $gs_start - 1; + $env_ar['GS'][$gs_ct]['stcount'] = trim($seg_ar[1]); // ST count + $gs_st_ct += $seg_ar[1]; + if ($seg_ar[2] != $env_ar['GS'][$gs_ct]['gsn']) { + $this->message[] = 'edih_x12_envelopes: GS-GE identifier mismatch' . PHP_EOL; + } + + if ($gs_ct === 0 && ($seg_ar[1] != count($env_ar['ST']))) { + $this->message[] = 'edih_x12_envelopes: GS count of ST mismatch' . PHP_EOL; + } elseif ($gs_st_ct != count($env_ar['ST'])) { + $this->message[] = 'edih_x12_envelopes: GS count of ST mismatch' . PHP_EOL; + } + + continue; + } + + if (strncmp($sn, 'IEA' . $de, 4) == 0) { + $seg_ar = explode($de, (string) $seg_text); + $env_ar['ISA'][$icn]['count'] = $isa_segs_ct; + $env_ar['ISA'][$icn]['gscount'] = $seg_ar[1]; + $iea_ct++; + if (count($env_ar['GS']) != $seg_ar[1]) { + $this->message[] = 'edih_x12_envelopes: GS count mismatch in ISA ' . \text($icn) . PHP_EOL; + $gsct = count($env_ar['GS']); + $this->message[] = 'GS group count: ' . \text($gsct) . ' IEA01: ' . \text($seg_ar[1]) . ' segment: ' . \text($seg_text); + } + + if ($env_ar['ISA'][$icn]['icn'] !== $seg_ar[2]) { + $this->message[] = 'edih_x12_envelopes: ISA-IEA identifier mismatch ISA ' . \text($icn) . ' IEA ' . \text($seg_ar[2]); + } + + if ($iea_ct == $isa_ct) { + $trnset_seg_ct += $isa_segs_ct; + //if ( $i+1 != $trnset_seg_ct ) { + if ($i != $trnset_seg_ct) { + $this->message[] = 'edih_x12_envelopes: IEA segment count error ' . \text($i) . ' : ' . \text($trnset_seg_ct); + } + } else { + $this->message[] = 'edih_x12_envelopes: ISA-IEA count mismatch ISA ' . \text($isa_ct) . ' IEA ' . \text($iea_ct); + } + + continue; + } + } + + return $env_ar; + } + + /** + * Parse x12 file contents into array of segments. + * + * @uses edih_x12_delimiters() + * @uses edih_x12_scan() + * + * @param string $file_text + * @return array array['i'] = segment, or empty on error + */ + public function edih_x12_segments($file_text = '') + { + $ar_seg = []; + // do verifications + if ($file_text) { + if (!$this->constructing) { + // need to validate file + $vars = $this->edih_file_text($file_text, false, true, false); + $f_str = $file_text; + $dt = $vars['delimiters']['t'] ?? ''; + } else { + $f_str = $file_text; + if (isset($this->delimiters['t'])) { + $dt = $this->delimiters['t']; + } else { + $delims = $this->edih_x12_delimiters(substr($f_str, 0, 126)); + $dt = $delims['t'] ?? ''; + } + } + } elseif ($this->text) { + $f_str = $this->text; + if (isset($this->delimiters['t'])) { + $dt = $this->delimiters['t']; + } else { + $delims = $this->edih_x12_delimiters(substr((string) $f_str, 0, 126)); + $dt = $delims['t'] ?? ''; + } + } else { + $this->message[] = 'edih_x12_segments: no file text'; + return $ar_seg; + } + + // did we get the segment terminator? + if (!$dt) { + $this->message[] = 'edih_x12_segments: invalid delimiters'; + return $ar_seg; + } + + // OK, now initialize variables + $seg_pos = 0; // position where segment begins + $seg_end = 0; + $seg_ct = 0; + $moresegs = true; + // could test this against simple $segments = explode($dt, $f_str) + while ($moresegs) { + // extract each segment from the file text + $seg_end = strpos((string) $f_str, (string) $dt, $seg_pos); + $seg_text = substr((string) $f_str, $seg_pos, $seg_end - $seg_pos); + $seg_pos = $seg_end + 1; + $moresegs = strpos((string) $f_str, (string) $dt, $seg_pos); + $seg_ct++; + // we trim in case there are line or carriage returns + $ar_seg[$seg_ct] = trim($seg_text); + } + + return $ar_seg; + } + + + /** + * extract the segments representing a transaction for CLM01 pt-encounter number + * note: there may be more than one in a file, all matching are returned + * 27x transactions will have unique BHT03 that could be used as the claimid argument + * + * return_array[i] => transaction segments array + * return_array[i][j] => particular segment string + * + * @param string $clm01 837 CLM01 or BHT03 from 277 + * @param string $stn ST number -- optional, limit search to that ST-SE envelope + * @param string $filetext optional file contents + * @return array multidimensional array of segments or empty on failure + */ + public function edih_x12_transaction($clm01, $stn = '', $filetext = '') + { + $ret_ar = []; + if (!$clm01) { + $this->message[] = 'edih_x12_transaction: invalid argument'; + return $ret_ar; + } + + $de = ''; + $tp = ''; + $seg_ar = []; + $env_ar = []; + // select the data to search + if ($filetext && !$this->constructing) { + $vars = $this->edih_file_text($filetext, true, true, true); + $tp = $vars['type'] ?? $tp; + $de = $vars['delimiters']['e'] ?? $de; + $seg_ar = $vars['segments'] ?? $seg_ar; + //$env_ar = $vars['envelopes']; // probably faster without envelopes in this case + } elseif (count($this->segments)) { + // default created object + $seg_ar = $this->segments; + if (count($this->delimiters)) { + $de = $this->delimiters['e']; + } else { + $de = (str_starts_with((string) reset($segment_ar), 'ISA')) ? substr((string) reset($segment_ar), 3, 1) : ''; + } + + $tp = $this->type ?: $this->edih_x12_type(); + $env_ar = ( isset($this->envelopes['ST']) ) ? $this->envelopes : $env_ar; + } elseif ($this->text) { + // object with file text, but no processing + $tp = $this->edih_x12_type(); + $seg_ar = ( $tp ) ? $this->edih_x12_segments() : $seg_ar; + if (count($seg_ar)) { + $de = substr((string) reset($seg_ar), 3, 1); + } + } else { + $this->message[] = 'edih_x12_transaction: invalid search data'; + return $ret_ar; + } + + if (!count($seg_ar)) { + $this->message[] = 'edih_x12_transaction: invalid segments'; + return $ret_ar; + } + + if (!$de) { + $this->message[] = 'edih_x12_transaction: invalid delimiters'; + return $ret_ar; + } + + //array('HB'=>'271', 'HS'=>'270', 'HR'=>'276', 'HI'=>'278', + // 'HN'=>'277', 'HP'=>'835', 'FA'=>'999', 'HC'=>'837'); + if (str_starts_with((string) $tp, 'mixed')) { + $tp = substr((string) $tp, -2); + } + + if (!strpos('|HB|271|HS|270|HR|276|HI|278|HN|277|HP|835|FA|999|HC|837', (string) $tp)) { + $this->message[] = 'edih_x12_transaction: wrong edi type for transaction search ' . \text($tp); + return $ret_ar; + } + + $idx = 0; + $is_found = false; + $slice = []; + $srch_ar = []; + $sl_idx = 0; + // there may be several in same ST envelope with the same $clm01, esp. 835 + // we will get each set of relevant transaction segments in foreach() below + if (count($env_ar)) { + foreach ($env_ar['ST'] as $st) { + if (strlen($stn) && $st['stn'] != $stn) { + continue; + } + + if (isset($st['acct']) && count($st['acct'])) { + $ky = array_search($clm01, $st['acct']); + if ($ky !== false) { + $srch_ar[$idx]['array'] = array_slice($seg_ar, $st['start'], $st['count'], true); + $srch_ar[$idx]['start'] = $st['start']; + $srch_ar[$idx]['type'] = $st['type']; + $idx++; + } + } + } + } + + // if not identified in envelope search, use segments + if (!count($srch_ar)) { + $srch_ar[0]['array'] = $seg_ar; + $srch_ar[0]['start'] = 0; // with array_slice() the index is absolute zero base + $srch_ar[0]['type'] = $tp; + } + + // verify we have type + if ($srch_ar[0]['type'] == 'NA' || !$srch_ar[0]['type']) { + $this->edih_message(); + return $ret_ar; + } + + // segments we check + $test_id = ['TRN','CLM','CLP','ST' . $de,'BHT','REF','LX' . $de,'PLB','SE' . $de]; + foreach ($srch_ar as $srch) { + $idx = $srch['start'] - 1; // align index to segments array offset + $type = (string)$srch['type']; + $is_found = false; + $idval = ''; + $idlen = 1; + foreach ($srch['array'] as $seg) { + $idx++; + $test_str = substr((string) $seg, 0, 3); + if (!in_array($test_str, $test_id, true)) { + continue; + } + + // the opening ST segment should be in each search array, + // so type and search values can be determined here. + if (strncmp((string) $seg, 'ST' . $de, 3) == 0) { + $stseg = explode($de, (string) $seg); + $type = $type ?: $stseg[1]; + $idval = ( strpos('|HN|277|HB|271', $type) ) ? 'TRN' . $de . '2' . $de . $clm01 : ''; + $idval = ( strpos('|HR|276|HS|270', $type) ) ? 'TRN' . $de . '1' . $de . $clm01 : $idval; + $idval = ( strpos('|HI|278', $type) ) ? 'REF' . $de . 'EJ' . $de . $clm01 : $idval; + $idval = ( strpos('|HC|837', $type) ) ? 'CLM' . $de . $clm01 . $de : $idval; + $idval = ( strpos('|HP|835', $type) ) ? 'CLP' . $de . $clm01 . $de : $idval; + $idlen = strlen($idval); + continue; + } + + //array('HB'=>'271', 'HS'=>'270', 'HR'=>'276', 'HI'=>'278', + // 'HN'=>'277', 'HP'=>'835', 'FA'=>'999', 'HC'=>'837'); + // these types use the BHT segment to begin transactions + if (strpos('|HI|278|HN|277|HR|276|HB|271|HS|270|HC|837', $type)) { + if (strncmp((string) $seg, 'BHT' . $de, 4) === 0) { + $bht_seg = explode($de, (string) $seg); + $bht_pos = $idx; + //$bht_pos = $key; + if ($is_found && isset($slice[$sl_idx]['start'])) { + $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; + //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; + $is_found = false; + $sl_idx++; + } elseif (strcmp($clm01, $bht_seg[3]) === 0) { + // matched by BHT03 identifier + $is_found = true; + $slice[$sl_idx]['start'] = $bht_pos; + } + + continue; + } + + if (strncmp((string) $seg, $idval, $idlen) === 0) { + // matched by clm01 identifier (idval) + $is_found = true; + $slice[$sl_idx]['start'] = $bht_pos; + continue; + } + } + + if ($type == 'HP' || $type == '835') { + if (strncmp((string) $seg, 'CLP' . $de, 4) === 0) { + if (strncmp((string) $seg, $idval, $idlen) === 0) { + if ($is_found && isset($slice[$sl_idx]['start'])) { + $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; + //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; + $sl_idx++; + } + + $is_found = true; + $slice[$sl_idx]['start'] = $idx; + //$slice[$sl_idx]['start'] = $key; + } else { + if ($is_found && isset($slice[$sl_idx]['start'])) { + $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; + //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; + $is_found = false; + $sl_idx++; + } + } + + continue; + } + + // LX segment is often used to group claim payment information + // we do not capture TS3 or TS2 segments in the transaction + if (strncmp((string) $seg, 'LX' . $de, 3) === 0) { + if ($is_found && isset($slice[$sl_idx]['start'])) { + $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; + //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; + $is_found = false; + $sl_idx++; + } + + continue; + } + + // PLB segment is part of summary/trailer in 835 + // not part of the preceding transaction + if (strncmp((string) $seg, 'PLB' . $de, 4) === 0) { + if ($is_found && isset($slice[$sl_idx]['start'])) { + $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; + //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; + $is_found = false; + $sl_idx++; + } + + continue; + } + } + + // SE will always mark end of transaction segments + if (strncmp((string) $seg, 'SE' . $de, 3) === 0) { + if ($is_found && isset($slice[$sl_idx]['start'])) { + $slice[$sl_idx]['count'] = $idx - $slice[$sl_idx]['start']; + //$slice[$sl_idx]['count'] = $key - $slice[$sl_idx]['start']; + $is_found = false; + $sl_idx++; + } + } + } // end foreach($srch['array'] as $seg) + } // end foreach($srch_ar as $srch) + if (count($slice)) { + foreach ($slice as $sl) { + $ret_ar[] = array_slice($seg_ar, $sl['start'], $sl['count'], true); + } + } + + return $ret_ar; + } + + + /** + * get the segment(s) with a particular ID, such as CLP, NM1, etc. + * return is array + * array[i] => matching segment string + * + * @param string $segmentID such as NM1, CLP, STC, etc. + * @param string $srchStr optional string contained in segment + * @param array $seg_array optional supplied array of segments to search + * @return array + */ + public function edih_get_segment($segmentID, $srchStr = '', $seg_array = '') + { + $ret_ar = []; + $seg_ar = []; + $segid = ( strlen($segmentID) ) ? trim($segmentID) : ''; + $srch = ( strlen($srchStr) ) ? $srchStr : ''; + + if (!$segid) { + $this->message[] = 'edih_get_segment(): missing segment ID'; + return $ret_ar; + } + + $de = $this->delimiters['e'] ?? ''; + $dt = $this->delimiters['t'] ?? ''; + // segment array from edih_x12_transaction() is two dimension + if (is_array($seg_array) && count($seg_array)) { + if (isset($seg_array[0]) && is_array($seg_array[0])) { + foreach ($seg_array as $ar) { + $seg_ar = array_merge($seg_ar, $ar); + } + } else { + $seg_ar = $seg_array; + } + } elseif (count($this->segments)) { + $seg_ar = $this->segments; + } elseif ($this->text) { + if (!$de) { + $delims = $this->edih_x12_delimiters(substr((string) $this->text, 0, 126)); + $dt = $delims['t'] ?? ''; + $de = $delims['e'] ?? ''; + } + + if (!$de || !$dt) { + $this->message[] = 'edih_get_segment() : unable to get delimiters'; + return $ret_ar; + } + + $segsrch = ($segid == 'ISA') ? $segid . $de : $dt . $segid . $de; + $seg_pos = 1; + $see_pos = 2; + while ($seg_pos) { + $seg_pos = strpos((string) $this->text, $segsrch, $seg_pos); + $see_pos = strpos((string) $this->text, (string) $dt, $seg_pos + 1); + if ($seg_pos) { + $segstr = trim(substr((string) $this->text, $seg_pos, $see_pos - $seg_pos), $dt); + if ($srch) { + if (str_contains($segstr, $srch)) { + $ret_ar[] = $segstr; + } + } else { + $ret_ar[] = $segstr; + } + + $seg_pos = $see_pos + 1; + } + } + } + + if (count($seg_ar)) { + $cmplen = strlen($segid . $de); + foreach ($seg_ar as $key => $seg) { + if (strncmp((string) $seg, $segid . $de, $cmplen) === 0) { + if ($srch) { + if (str_contains((string) $seg, $srch)) { + $ret_ar[$key] = $seg; + } + } else { + $ret_ar[$key] = $seg; + } + } + } + } else { + $this->message[] = 'edih_get_segment() : no segments or text content available'; + } + + return $ret_ar; + } + + + /** + * Get a slice of the segments array + * Supply an array with one or more of the following keys and values: + * + * ['trace'] => trace value from 835(TRN02) or 999(TA101) x12 type + * ['ISA13'] => ISA13 + * ['GS06'] => GS06 (sconsider also 'ISA13') + * ['ST02'] => ST02 (condider also 'ISA13' and 'GS06') + * ['keys'] => true to preserve segment numbering from original file + * + * The return value will be an array of one or more segments. + * The 'search' parameter results in one or more segments containing + * the search string. The + * @param array note: all element values except 'keys' are strings + * @return array + */ + function edih_x12_slice($arg_array, $file_text = '') + { + $ret_ar = []; + $f_str = ''; + // see what we have + if (!is_array($arg_array) || !count($arg_array)) { + // debug + $this->message[] = 'edih_x12_slice() invalid array argument'; + return $ret_ar; + } + + if ($file_text) { + // need to validate file edih_file_text($file_text, $type=false, $delimiters=false, $segments=false) + $vars = $this->edih_file_text($file_text, true, true, false); + if (is_array($vars) && count($vars)) { + $f_str = $file_text; + $dt = $vars['delimiters']['t'] ?? ''; + $de = $vars['delimiters']['e'] ?? ''; + $ft = $vars['type'] ?? ''; + //$seg_ar = ( isset($vars['segments']) ) ? $vars['segments'] : ''; + //$env_ar = $this->edih_x12_envelopes($f_str); + } else { + $this->message[] = 'edih_x12_slice() error processing file text'; + // debug + //echo $this->edih_message().PHP_EOL; + return $ret_ar; + } + } elseif (count($this->segments) && count($this->envelopes) && count($this->delimiters)) { + $seg_ar = $this->segments; + $env_ar = $this->envelopes; + $dt = $this->delimiters['t']; + $de = $this->delimiters['e']; + $ft = $this->type; + } else { + $this->message[] = 'edih_x12_slice() object missing needed properties'; + // debug + //echo $this->edih_message().PHP_EOL; + return $ret_ar; + } + + // initialize search variables + $trace = ''; + $stn = ''; + $gsn = ''; + $icn = ''; + $prskeys = false; + foreach ($arg_array as $key => $val) { + switch ((string)$key) { + case 'trace': + $trace = (string)$val; + break; + case 'ST02': + $stn = (string)$val; + break; + case 'GS06': + $gsn = (string)$val; + break; + case 'ISA13': + $icn = (string)$val; + break; + case 'keys': + $prskeys = (bool)$val; + break; + } + } + + if ($trace && !str_contains('|HP|FA', (string) $ft)) { + $this->message[] = 'edih_x12_slice() incorrect type [' . \text($ft) . '] for trace'; + return $ret_ar; + } + + if ($f_str) { + $srchstr = ''; + if ($icn) { + $icnpos = strpos((string) $f_str, $de . $icn . $de); + if ($icnpos === false) { + // $icn not found + $this->message[] = 'edih_x12_slice() did not find ISA13 ' . \text($icn); + // debug + //echo $this->edih_message().PHP_EOL; + return $ret_ar; + } elseif ($icnpos < 106) { + $isapos = 0; + } else { + $isapos = strrpos((string) $f_str, $dt . 'ISA' . $de, ($icnpos - strlen((string) $f_str))) + 1; + } + + $ieapos = strpos((string) $f_str, $de . $icn . $dt, $isapos); + $ieapos = strpos((string) $f_str, (string) $dt, $ieapos) + 1; + $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $isapos + 2) + 1 : 0; + $srchstr = substr((string) $f_str, $isapos, $ieapos - $isapos); + } + + if ($gsn) { + $srchstr = $srchstr ?: $f_str; + $gspos = strpos((string) $srchstr, $de . $gsn . $de); + if ($gspos === false) { + // $gsn not found + $this->message[] = 'edih_x12_slice() did not find GS06 ' . \text($gsn); + return $ret_ar; + } else { + $gspos = strrpos(substr((string) $srchstr, 0, $gspos), (string) $dt) + 1; + } + + $gepos = strpos((string) $srchstr, $dt . 'GE' . $dt, $gspos); + $gepos = strpos((string) $srchstr, (string) $dt, $gepos + 1) + 1; + $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $gspos + 2) + 1 : 0; + $srchstr = substr((string) $srchstr, $gspos, $gepos - $gspos); + } + + if ($stn) { + $srchstr = $srchstr ?: $f_str; + $sttp = $this->gstype_ar[$ft]; + $seg_st = $dt . 'ST' . $de . $sttp . $de . $stn ; + $seg_se = $dt . 'SE' . $de; + // $segpos = 1; + $stpos = strpos((string) $srchstr, $seg_st); + if ($stpos === false) { + // $stn not found + $this->message[] = 'edih_x12_slice() did not find ST02 ' . \text($stn); + return $ret_ar; + } else { + $stpos += 1; + } + + $sepos = strpos((string) $srchstr, $seg_se, $stpos); + $sepos = strpos((string) $srchstr, (string) $dt, $sepos + 1); + $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $stpos + 2) + 1 : 0; + $srchstr = substr((string) $srchstr, $stpos, $sepos - $stpos); + } + + if ($trace) { + $trpos = strpos((string) $f_str, $de . $trace); + if ($trpos === false) { + // $icn not found + $this->message[] = 'edih_x12_slice() did not find trace ' . \text($trace); + return $ret_ar; + } + + $sttp = $this->gstype_ar[$ft]; + $seg_st = $dt . 'ST' . $de . $sttp . $de; + $stpos = strrpos((string) $f_str, $seg_st, ($trpos - strlen((string) $f_str))); + $sepos = strpos((string) $f_str, $dt . 'SE' . $de, $stpos); + $sepos = strpos((string) $f_str, (string) $dt, $sepos + 1); + $segidx = ($prskeys) ? substr_count((string) $f_str, (string) $dt, 0, $st_pos + 2) + 1 : 0; + $srchstr = substr((string) $f_str, $stpos + 1, $sepos - $stpos); + } + + // if we have a match, the $srchstr should have the desired segments + if ($trace || $icn || $gsn || $stn) { + if ($srchstr) { + $seg_ar = explode($dt, $srchstr); + // to keep segment numbers same as original file + foreach ($seg_ar as $seg) { + $ret_ar[$segidx] = $seg; + $segidx++; + } + + return $ret_ar; + } else { + $this->message[] = 'edih_x12_slice() error creating substring'; + return $ret_ar; + } + } + + // file_text not supplied, check for object values + } elseif (!($seg_ar && $env_ar && $dt && $de && $ft)) { + // debug + $this->message[] = 'edih_x12_slice() error is processing file'; + return $ret_ar; + } + + // file_text not supplied, use object values + if ($trace) { + foreach ($env_ar['ST'] as $st) { + if ($st['trace'] == $trace) { + // have to add one to the count to capture the SE segment so html_str has data + // when called from edih_835_payment_html function in edih_835_html.php 4-25-17 SMW + $ret_ar = array_slice($seg_ar, $st['start'], $st['count'] + 1, $prskeys); + break; + } + } + } elseif ($icn && !($stn || $gsn)) { + if (isset($env_ar['ISA'][$icn])) { + $ret_ar = array_slice($seg_ar, $env_ar['ISA'][$icn]['start'], $env_ar['ISA'][$icn]['count'], $prskeys); + } + } elseif ($gsn && !$stn) { + foreach ($env_ar['GS'] as $gs) { + if ($icn) { + if (($gs['icn'] == $icn) && ($gs['gsn'] == $gsn)) { + $ret_ar = array_slice($seg_ar, $gs['start'], $gs['count'], $prskeys); + break; + } + } else { + if ($gs['gsn'] == $gsn) { + $ret_ar = array_slice($seg_ar, $gs['start'], $gs['count'], $prskeys); + break; + } + } + } + } elseif ($stn) { + // ; + foreach ($env_ar['ST'] as $st) { + if ($icn) { + if ($gsn) { + if ($st['icn'] == $icn && $st['gsn'] == $gsn && $st['stn'] == $stn) { + $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); + break; + } + } else { + if ($st['icn'] == $icn && $st['stn'] == $stn) { + $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); + break; + } + } + } elseif ($gsn) { + if ($st['gsn'] == $gsn && $st['stn'] == $stn) { + $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); + break; + } + } elseif ($st['stn'] == $stn) { + $ret_ar = array_slice($seg_ar, $st['start'], $st['count'], $prskeys); + break; + } + } + } else { + $this->message[] = 'edih_x12_slice() no file text or invalid array argument keys or values'; + } + + if (!count($ret_ar)) { + $this->message[] = 'edih_x12_slice() no match'; + } + + return $ret_ar; + } + +// end class X12File +} diff --git a/tests/Tests/Isolated/Billing/EdiHistory/X12FileIsolatedTest.php b/tests/Tests/Isolated/Billing/EdiHistory/X12FileIsolatedTest.php new file mode 100644 index 000000000000..0d4e411d61ba --- /dev/null +++ b/tests/Tests/Isolated/Billing/EdiHistory/X12FileIsolatedTest.php @@ -0,0 +1,331 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc. + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +declare(strict_types=1); + +namespace OpenEMR\Tests\Isolated\Billing\EdiHistory; + +use OpenEMR\Billing\EdiHistory\X12File; +use PHPUnit\Framework\TestCase; + +// X12File renders error messages through the global text() helper that +// normally lives in library/htmlspecialchars.inc.php. Stub it here so the +// isolated test does not require the legacy bootstrap. The stub is invoked +// by tests that exercise error paths (e.g. invalid file path), but function +// declarations themselves never register as covered lines, so ignore it. +// @codeCoverageIgnoreStart +if (!function_exists('text')) { + function text(?string $value): string + { + return htmlspecialchars($value ?? '', ENT_NOQUOTES); + } +} +// @codeCoverageIgnoreEnd + +class X12FileIsolatedTest extends TestCase +{ + private string $fixturePath; + private string $fixtureText; + + protected function setUp(): void + { + $this->fixtureText = $this->build270(); + $this->fixturePath = tempnam(sys_get_temp_dir(), 'x12file_test_'); + file_put_contents($this->fixturePath, $this->fixtureText); + } + + private function build270(): string + { + $isa = 'ISA*00* *00* ' + . '*ZZ*SENDER *ZZ*RECEIVER ' + . '*240101*1200*^*00501*000000001*0*T*:~'; + $body = 'GS*HS*SENDER*RECEIVER*20240101*1200*1*X*005010X279A1~' + . 'ST*270*0001*005010X279A1~' + . 'BHT*0022*13*REF1*20240101*1200~' + . 'HL*1**20*1~' + . 'NM1*PR*2*INSCO*****PI*12345~' + . 'HL*2*1*21*1~' + . 'NM1*1P*2*PROV*****XX*1234567890~' + . 'HL*3*2*22*0~' + . 'TRN*1*REF*1234567890~' + . 'NM1*IL*1*DOE*JOHN****MI*123456789~' + . 'DMG*D8*19800101*M~' + . 'DTP*291*D8*20240101~' + . 'EQ*30~' + . 'SE*12*0001~' + . 'GE*1*1~' + . 'IEA*1*000000001~'; + + return $isa . $body; + } + + protected function tearDown(): void + { + if (is_file($this->fixturePath)) { + unlink($this->fixturePath); + } + } + + public function testEmptyConstructorProducesUsableObject(): void + { + $x = new X12File(); + $this->assertSame('', $x->edih_filepath()); + $this->assertSame('', $x->edih_filename()); + $this->assertFalse($x->edih_valid()); + $this->assertFalse($x->edih_isx12()); + $this->assertSame([], $x->edih_segments()); + $this->assertSame([], $x->edih_envelopes()); + } + + public function testInvalidFilePathReportsMessageAndStaysInvalid(): void + { + $x = new X12File('/nonexistent/path/should/not/resolve.x12'); + $this->assertFalse($x->edih_valid()); + $this->assertStringContainsString( + 'invalid file path', + $x->edih_message(), + ); + } + + public function testFixtureIsRecognisedAsValidX12(): void + { + $x = new X12File($this->fixturePath); + $this->assertTrue($x->edih_valid(), 'fixture should pass scan'); + $this->assertTrue($x->edih_isx12(), 'starts with ISA'); + $this->assertTrue($x->edih_hasGS(), 'has GS envelope'); + $this->assertTrue($x->edih_hasST(), 'has ST envelope'); + $this->assertSame(basename($this->fixturePath), $x->edih_filename()); + $this->assertSame($this->fixturePath, $x->edih_filepath()); + $this->assertSame('00501', $x->edih_version()); + } + + public function testDelimitersDetectedFromIsaSegment(): void + { + $x = new X12File($this->fixturePath); + $delim = $x->edih_delimiters(); + self::assertIsArray($delim); + $this->assertSame('*', $delim['e'] ?? null, 'element separator'); + $this->assertSame('~', $delim['t'] ?? null, 'segment terminator'); + $this->assertSame(':', $delim['s'] ?? null, 'sub-element separator'); + } + + public function testTransactionTypeReturnsGsFunctionalIdCode(): void + { + $x = new X12File($this->fixturePath); + // edih_type() returns the GS01 functional identifier code itself + // ('HS' for a 270 inquiry); the gstype_ar lookup table maps it to + // the transaction-set number elsewhere. + $this->assertSame('HS', $x->edih_type()); + } + + public function testSegmentsAndEnvelopesPopulatedAfterParsing(): void + { + $x = new X12File($this->fixturePath); + $segments = $x->edih_segments(); + $this->assertNotEmpty($segments); + + $envelopes = $x->edih_envelopes(); + $this->assertNotEmpty($envelopes, 'envelope summary should be populated'); + } + + public function testTextNotRetainedByDefault(): void + { + $x = new X12File($this->fixturePath); + $this->assertSame('', $x->edih_text(), 'default $text=false should drop file body'); + $this->assertGreaterThan(0, $x->edih_length(), 'length recorded even when text dropped'); + } + + public function testTextRetainedWhenRequested(): void + { + $x = new X12File($this->fixturePath, true, true); + $text = $x->edih_text(); + self::assertIsString($text); + $this->assertNotSame('', $text); + $this->assertSame(strlen($text), $x->edih_length()); + } + + public function testNoMkSegsConstructorPathPopulatesTypeFromText(): void + { + // mk_segs=false skips segment building and instead derives type by + // scanning the file body for GS segments via edih_x12_type($text). + $x = new X12File($this->fixturePath, false, false); + $this->assertTrue($x->edih_valid()); + $this->assertSame([], $x->edih_segments(), 'no segments built'); + $this->assertSame('HS', $x->edih_type(), 'type derived from GS scan of text'); + } + + public function testEdihGsTypeMapsKnownGs01Codes(): void + { + $x = new X12File(); + $this->assertSame('837', $x->edih_gs_type('HC')); + $this->assertSame('835', $x->edih_gs_type('HP')); + $this->assertSame('270', $x->edih_gs_type('HS')); + $this->assertSame('999', $x->edih_gs_type('FA')); + // case-insensitive lookup + $this->assertSame('837', $x->edih_gs_type('hc')); + } + + public function testEdihGsTypeReturnsFalseForUnknownCode(): void + { + $x = new X12File(); + $this->assertFalse($x->edih_gs_type('XX')); + } + + public function testEdihMessageReturnsEmptyStringWhenNoMessages(): void + { + $x = new X12File(); + $this->assertSame('', $x->edih_message()); + } + + public function testEdihMessageRendersAccumulatedMessagesAsHtml(): void + { + $x = new X12File('/nonexistent/path/file.x12'); + $html = $x->edih_message(); + $this->assertStringContainsString('

', $html); + $this->assertStringContainsString('
', $html); + $this->assertStringContainsString('invalid file path', $html); + } + + public function testScanReturnsEmptyOnZeroLengthInput(): void + { + $x = new X12File(); + $this->assertSame('', $x->edih_x12_scan('')); + $this->assertStringContainsString('zero length', $x->edih_message()); + } + + public function testScanStripsInternalNewlinesBeforeProcessing(): void + { + // Internal PHP_EOL inside an otherwise-valid X12 file should be + // removed before mime/regex checks; the file should still scan as + // a valid x12 with ISA/GS/ST envelopes ('ovigs'). + $x = new X12File(); + $multiline = 'ISA*00* *00* ' + . '*ZZ*SENDER *ZZ*RECEIVER ' + . '*240101*1200*^*00501*000000001*0*T*:~' . PHP_EOL + . 'GS*HS*S*R*20240101*1200*1*X*005010X279A1~' . PHP_EOL + . 'ST*270*0001*005010X279A1~' . PHP_EOL + . 'SE*1*0001~GE*1*1~IEA*1*000000001~'; + $this->assertSame('ovigs', $x->edih_x12_scan($multiline)); + } + + public function testScanRejectsBinaryContentViaMimeCheck(): void + { + // PNG signature is unmistakably binary; finfo classifies it as + // image/png, which fails the text/plain;us-ascii requirement. + $x = new X12File(); + $png = "\x89PNG\r\n\x1a\nbinary garbage and more padding to be safe"; + $this->assertSame('', $x->edih_x12_scan($png)); + $this->assertStringContainsString('invalid mime info', $x->edih_message()); + } + + public function testScanRejectsSuspectCharacterPatterns(): void + { + // The suspect-pattern regex catches ${ along with assertSame('', $x->edih_x12_scan($payload)); + $this->assertStringContainsString('suspect characters', $x->edih_message()); + } + + public function testTypeFromTextArgumentOnConstructedObject(): void + { + // Calling edih_x12_type() with explicit text after construction + // exercises the !$this->constructing branch and the GS preg_match + // path. + $x = new X12File($this->fixturePath); + $this->assertSame('HS', $x->edih_x12_type($this->fixtureText)); + } + + public function testTypeReturnsFalseWhenTextHasNoGsSegment(): void + { + // A scan-valid ISA/ST file with no recognized GS functional ID + // should fall through to "error in identifying type" → false. + $x = new X12File(); + $noGs = 'ISA*00* *00* ' + . '*ZZ*SENDER *ZZ*RECEIVER ' + . '*240101*1200*^*00501*000000001*0*T*:~' + . 'ST*270*0001*005010X279A1~SE*1*0001~IEA*1*000000001~'; + // edih_x12_type's PHPDoc claims @return string but the + // implementation returns false on this error path; assert via the + // accumulated message which is the observable contract. + $x->edih_x12_type($noGs); + $this->assertStringContainsString('error in identifying type', $x->edih_message()); + } + + public function testDelimitersRejectsTooShortIsaString(): void + { + $x = new X12File(); + $this->assertSame([], $x->edih_x12_delimiters('ISA*too*short')); + $this->assertStringContainsString('too short', $x->edih_message()); + } + + public function testDelimitersRejectsNonIsaPrefix(): void + { + // 106+ char string that does not begin with ISA. + $x = new X12File(); + $padded = str_repeat('X', 106); + $this->assertSame([], $x->edih_x12_delimiters($padded)); + $this->assertStringContainsString('does not begin with ISA', $x->edih_message()); + } + + public function testDelimitersRejectsTruncatedIsaWithTooFewElements(): void + { + // Starts with ISA, length >= 106, but does not contain 16 + // element separators, so the parser bails on "too few elements". + $x = new X12File(); + $truncated = 'ISA*00*' . str_repeat('A', 110); + $this->assertSame([], $x->edih_x12_delimiters($truncated)); + $this->assertStringContainsString('too few elements', $x->edih_message()); + } + + public function testDelimitersHandleNon5010IsaWithoutRepetitionSeparator(): void + { + // A 4010 ISA segment puts no repetition separator at element 11; the + // delim_ct == 12 branch resets $dr to '' when ISA12 lacks '501'. + $isa4010 = 'ISA*00* *00* ' + . '*ZZ*SENDER *ZZ*RECEIVER ' + . '*240101*1200*U*00401*000000001*0*T*:~'; + $x = new X12File(); + $delim = $x->edih_x12_delimiters($isa4010); + $this->assertSame('*', $delim['e'] ?? null); + $this->assertSame('~', $delim['t'] ?? null); + $this->assertSame(':', $delim['s'] ?? null); + $this->assertSame('', $delim['r'] ?? null, '4010 has no repetition separator'); + } + + public function testEnvelopesWarnOnUnknownGsFunctionalIdCode(): void + { + // Build a fixture whose GS01 (ZZ) is not in the gstype_ar map. + $isa = 'ISA*00* *00* ' + . '*ZZ*SENDER *ZZ*RECEIVER ' + . '*240101*1200*^*00501*000000001*0*T*:~'; + $body = 'GS*ZZ*SENDER*RECEIVER*20240101*1200*1*X*005010~' + . 'ST*999*0001~SE*1*0001~GE*1*1~IEA*1*000000001~'; + $path = tempnam(sys_get_temp_dir(), 'x12file_unknown_gs_'); + file_put_contents($path, $isa . $body); + try { + $x = new X12File($path); + $this->assertStringContainsString('Unknown GS type', $x->edih_message()); + } finally { + unlink($path); + } + } +} From a9898fba11a9ef5170fae18cfb1c28b7f6f6e773 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 14:16:13 -0400 Subject: [PATCH 21/82] fix(db): convert declne_to_specfy in patient_data language and ethnicity (#11876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Follow-up to #10762, which renamed the misspelled `declne_to_specfy` race value to `decline_to_specify`. Brady's review on that PR flagged two issues in `sql/8_0_0-to-8_1_0_upgrade.sql` (originally `8_0_0-to-8_0_1_upgrade.sql`) that were never addressed before merge: - The `patient_data.race` UPDATE was guarded by `#IfColumn patient_data race`, which is always true — the UPDATE ran on every upgrade even when there was nothing to convert. Switch to `#IfRow patient_data race declne_to_specfy` so it's skipped when no rows match. - The same misspelled value can also appear in `patient_data.language` and `patient_data.ethnicity`. The `list_options` side already covers all three list_ids; this PR mirrors that on the patient row side. Refs https://github.com/openemr/openemr/issues/10385, follow-up to https://github.com/openemr/openemr/pull/10762. ## Test plan - [ ] Fresh install of 8.0.0 → upgrade to current master succeeds - [ ] Tenant with `declne_to_specfy` in `patient_data.race` / `language` / `ethnicity` is migrated to `decline_to_specify` - [ ] Tenant with no matching rows skips all three `#IfRow` blocks --- sql/8_0_0-to-8_1_0_upgrade.sql | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sql/8_0_0-to-8_1_0_upgrade.sql b/sql/8_0_0-to-8_1_0_upgrade.sql index aac91210a319..af5622450ec8 100644 --- a/sql/8_0_0-to-8_1_0_upgrade.sql +++ b/sql/8_0_0-to-8_1_0_upgrade.sql @@ -140,14 +140,23 @@ ALTER TABLE `facility` ADD `organization_type` VARCHAR(50) NOT NULL DEFAULT 'pro -- -- Rename the misspelled list_options option_id from 'declne_to_specfy' to 'decline_to_specify', --- and update any patient_data.race records that reference the old value. +-- and update any patient_data.race, patient_data.language, and patient_data.ethnicity +-- records that reference the old value. -- See: https://github.com/openemr/openemr/issues/10385 -- -#IfColumn patient_data race +#IfRow patient_data race declne_to_specfy UPDATE `patient_data` SET `race` = 'decline_to_specify' WHERE `race` = 'declne_to_specfy'; #EndIf +#IfRow patient_data language declne_to_specfy +UPDATE `patient_data` SET `language` = 'decline_to_specify' WHERE `language` = 'declne_to_specfy'; +#EndIf + +#IfRow patient_data ethnicity declne_to_specfy +UPDATE `patient_data` SET `ethnicity` = 'decline_to_specify' WHERE `ethnicity` = 'declne_to_specfy'; +#EndIf + #IfRow2D list_options list_id race option_id declne_to_specfy UPDATE `list_options` SET `option_id` = 'decline_to_specify' WHERE `list_id` = 'race' AND `option_id` = 'declne_to_specfy'; #EndIf From d990b4558893af651d3dadf38e20fa1a7da66b47 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 14:43:13 -0400 Subject: [PATCH 22/82] fix(csrf): stop rotating CSRF private key on every main_screen.php load (#11888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Loading `interface/main/main_screen.php` unconditionally regenerated the per-session CSRF private key, silently invalidating CSRF tokens already embedded in pages and iframes rendered before that load. Long-lived widgets that bake their token in at render time and poll on a fixed interval — most visibly `interface/main/dated_reminders/dated_reminders.php` (60s poll) — then logged `OpenEMR CSRF token authentication error` on every poll following any in-app reload of the top frame. The rotation looks like it was meant only for the new-login path. This PR moves `CsrfUtils::setupCsrfKey($session)` inside the new-login branch so the key is generated once at login and not rotated on subsequent top-frame loads. The `else` branch is reached only after a successful CSRF check, which already requires an existing private key, so no key setup is needed there. Fixes #11865. ## Test plan - [ ] Log in, capture the CSRF token rendered into `dated_reminders.php`, reload `main_screen.php`, then re-POST the same token to `dated_reminders.php` — should now return 200 instead of 403. - [ ] Confirm fresh login still works (the new-login branch still runs `setupCsrfKey`). - [ ] Confirm `OpenEMR CSRF token authentication error` log entries no longer appear at the 60s polling cadence after navigating between top frames. --- interface/main/main_screen.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/interface/main/main_screen.php b/interface/main/main_screen.php index f2930ddb2fde..d1920d60d341 100644 --- a/interface/main/main_screen.php +++ b/interface/main/main_screen.php @@ -8,9 +8,11 @@ * @author Rod Roark * @author Brady Miller * @author Ranganath Pathak + * @author Michael A. Smith * @copyright Copyright (c) 2018 Rod Roark * @copyright Copyright (c) 2018-2019 Brady Miller * @copyright Copyright (c) 2019 Ranganath Pathak + * @copyright Copyright (c) 2026 OpenCoreEMR Inc * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 */ @@ -382,14 +384,17 @@ function generate_html_end() } else { $_POST["clearPass"] = ''; } + // Set up the csrf private_key + // Note this key always remains private and never leaves server session. It is used to create + // the csrf tokens. + // Generated only on the new-login path. Rotating it on every main_screen.php load + // invalidates CSRF tokens already embedded in long-lived iframes (e.g. dated_reminders, + // which polls every 60s with the token captured at render time). + CsrfUtils::setupCsrfKey($session); } else { CsrfUtils::checkCsrfInput(INPUT_POST, dieOnFail: true); $session->migrate(false); } -// Set up the csrf private_key -// Note this key always remains private and never leaves server session. It is used to create -// the csrf tokens. -CsrfUtils::setupCsrfKey($session); // Set up the session uuid. This will be used for mapping session setting to database. // At this time only used for lastupdate tracking SessionTracker::setupSessionDatabaseTracker(); From 579cb46833f5cae490ab9925faa209c343716f18 Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 15:29:57 -0400 Subject: [PATCH 23/82] refactor: replace literal preg_match prefix/suffix checks with native string functions (#11884) Closes #11880. Convert `preg_match` calls that only match a literal prefix or suffix to `str_starts_with()` / `str_ends_with()`, or `pathinfo()` for file-extension checks. Skip cases that use capture groups, character classes, or alternation that can't be expressed cleanly without a regex. In `SQLUpgradeService::clickOptionsMigrate()`, guard against `fgets()` returning `false` explicitly rather than letting it coerce silently into `preg_match`. The matching baseline ignore is dropped. ## Test plan - [x] `composer phpcs` - [x] `composer phpstan` (baseline shrinks by one entry) - [x] `composer php-syntax-check` - [ ] CI green --- .phpstan/baseline/argument.type.php | 22 +++--- .phpstan/baseline/binaryOp.invalid.php | 2 +- .phpstan/baseline/cast.string.php | 14 ++-- .phpstan/baseline/empty.notAllowed.php | 2 +- .../baseline/encapsedStringPart.nonString.php | 15 ---- .phpstan/baseline/missingType.parameter.php | 15 ---- .../offsetAccess.nonOffsetAccessible.php | 4 +- .phpstan/baseline/offsetAccess.notFound.php | 5 -- .../openemr.forbiddenRequestGlobals.php | 6 +- interface/billing/edi_271.php | 12 +-- interface/fax/fax_dispatch.php | 11 +-- interface/forms/vitals/growthchart/chart.php | 10 ++- interface/patient_file/letter.php | 10 +-- interface/patient_file/pos_checkout_ippf.php | 25 +++--- .../patient_file/pos_checkout_normal.php | 2 +- interface/reports/appt_encounter_report.php | 2 +- interface/reports/ippf_statistics.php | 76 +++++++++---------- interface/super/edit_list.php | 12 +-- library/clinical_rules.php | 4 +- library/options.inc.php | 4 +- portal/home.php | 13 ++-- portal/import_template.php | 4 +- portal/patient/fwk/libs/util/parsecsv.lib.php | 4 +- src/Services/Utils/SQLUpgradeService.php | 42 +++++----- 24 files changed, 142 insertions(+), 174 deletions(-) diff --git a/.phpstan/baseline/argument.type.php b/.phpstan/baseline/argument.type.php index 03a60e4bc491..68cd1bec4aa2 100644 --- a/.phpstan/baseline/argument.type.php +++ b/.phpstan/baseline/argument.type.php @@ -3366,11 +3366,6 @@ 'count' => 3, 'path' => __DIR__ . '/../../interface/fax/fax_dispatch.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function attr_url expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/fax/fax_dispatch.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<1, max\\> given\\.$#', 'count' => 1, @@ -18996,6 +18991,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\$code of function process_ippf_code expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#2 \\$row of function ippfLoadColumnData expects array, mixed given\\.$#', 'count' => 5, @@ -19933,7 +19933,12 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr expects string, mixed given\\.$#', - 'count' => 34, + 'count' => 33, + 'path' => __DIR__ . '/../../interface/super/edit_list.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function attr expects string, string\\|null given\\.$#', + 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_list.php', ]; $ignoreErrors[] = [ @@ -72731,11 +72736,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../src/Services/Utils/SQLUpgradeService.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../src/Services/Utils/SQLUpgradeService.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#2 \\$subject of function preg_split expects string, string\\|false given\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/binaryOp.invalid.php b/.phpstan/baseline/binaryOp.invalid.php index d9493fe80e96..326380330fd0 100644 --- a/.phpstan/baseline/binaryOp.invalid.php +++ b/.phpstan/baseline/binaryOp.invalid.php @@ -1773,7 +1773,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between non\\-falsy\\-string and mixed results in an error\\.$#', - 'count' => 8, + 'count' => 7, 'path' => __DIR__ . '/../../interface/fax/fax_dispatch.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/cast.string.php b/.phpstan/baseline/cast.string.php index 737d0e2de81e..b5d0a3f57b35 100644 --- a/.phpstan/baseline/cast.string.php +++ b/.phpstan/baseline/cast.string.php @@ -158,7 +158,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 5, + 'count' => 1, 'path' => __DIR__ . '/../../interface/billing/edi_271.php', ]; $ignoreErrors[] = [ @@ -313,7 +313,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 14, + 'count' => 12, 'path' => __DIR__ . '/../../interface/fax/fax_dispatch.php', ]; $ignoreErrors[] = [ @@ -478,7 +478,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 25, + 'count' => 22, 'path' => __DIR__ . '/../../interface/forms/vitals/growthchart/chart.php', ]; $ignoreErrors[] = [ @@ -1383,7 +1383,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 19, + 'count' => 16, 'path' => __DIR__ . '/../../interface/patient_file/pos_checkout_ippf.php', ]; $ignoreErrors[] = [ @@ -1653,7 +1653,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 44, + 'count' => 9, 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', ]; $ignoreErrors[] = [ @@ -2668,7 +2668,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 7, + 'count' => 5, 'path' => __DIR__ . '/../../portal/home.php', ]; $ignoreErrors[] = [ @@ -3848,7 +3848,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 9, + 'count' => 8, 'path' => __DIR__ . '/../../src/Services/Utils/SQLUpgradeService.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/empty.notAllowed.php b/.phpstan/baseline/empty.notAllowed.php index 11fe1f644054..a6a32dfe3c9c 100644 --- a/.phpstan/baseline/empty.notAllowed.php +++ b/.phpstan/baseline/empty.notAllowed.php @@ -1798,7 +1798,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 93, + 'count' => 92, 'path' => __DIR__ . '/../../interface/patient_file/pos_checkout_ippf.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/encapsedStringPart.nonString.php b/.phpstan/baseline/encapsedStringPart.nonString.php index d284eba1cf4b..cb828c428e88 100644 --- a/.phpstan/baseline/encapsedStringPart.nonString.php +++ b/.phpstan/baseline/encapsedStringPart.nonString.php @@ -511,16 +511,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/fax/fax_dispatch.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$ffname \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/fax/fax_dispatch.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$filename \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/fax/fax_dispatch.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$inbase \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 1, @@ -3276,11 +3266,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../interface/super/edit_layout_props.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$list_id \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_list.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 2, diff --git a/.phpstan/baseline/missingType.parameter.php b/.phpstan/baseline/missingType.parameter.php index 9f1d9947229b..920ec16c2ab6 100644 --- a/.phpstan/baseline/missingType.parameter.php +++ b/.phpstan/baseline/missingType.parameter.php @@ -14791,16 +14791,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Function getAbortionMethod\\(\\) has parameter \\$code with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Function getContraceptiveMethod\\(\\) has parameter \\$code with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', -]; $ignoreErrors[] = [ 'message' => '#^Function getGcacClientStatus\\(\\) has parameter \\$row with no type specified\\.$#', 'count' => 1, @@ -14856,11 +14846,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Function process_ippf_code\\(\\) has parameter \\$code with no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/reports/ippf_statistics.php', -]; $ignoreErrors[] = [ 'message' => '#^Function process_ippf_code\\(\\) has parameter \\$quantity with no type specified\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php index aa1bc207acfb..b7bf3619b00b 100644 --- a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php +++ b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php @@ -2048,7 +2048,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'name\' on mixed\\.$#', - 'count' => 5, + 'count' => 1, 'path' => __DIR__ . '/../../interface/billing/edi_271.php', ]; $ignoreErrors[] = [ @@ -37343,7 +37343,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'pc_startTime\' on mixed\\.$#', - 'count' => 4, + 'count' => 2, 'path' => __DIR__ . '/../../portal/home.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/offsetAccess.notFound.php b/.phpstan/baseline/offsetAccess.notFound.php index 90a8df052cde..9fce0aef49b9 100644 --- a/.phpstan/baseline/offsetAccess.notFound.php +++ b/.phpstan/baseline/offsetAccess.notFound.php @@ -131,11 +131,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/forms/vitals/growthchart/chart.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'sex\' might not exist on \'\'\\|array\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/forms/vitals/growthchart/chart.php', -]; $ignoreErrors[] = [ 'message' => '#^Offset 1 might not exist on array\\{\\}\\|array\\{non\\-falsy\\-string, non\\-falsy\\-string&numeric\\-string, non\\-falsy\\-string&numeric\\-string, non\\-falsy\\-string&numeric\\-string\\}\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/openemr.forbiddenRequestGlobals.php b/.phpstan/baseline/openemr.forbiddenRequestGlobals.php index b88f22614655..dcfba3923a1c 100644 --- a/.phpstan/baseline/openemr.forbiddenRequestGlobals.php +++ b/.phpstan/baseline/openemr.forbiddenRequestGlobals.php @@ -418,7 +418,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Direct access to \\$_FILES is forbidden\\. Use Symfony\'s Request object or filter_input\\(\\) instead\\.$#', - 'count' => 10, + 'count' => 6, 'path' => __DIR__ . '/../../interface/billing/edi_271.php', ]; $ignoreErrors[] = [ @@ -2758,7 +2758,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Direct access to \\$_POST is forbidden\\. Use Symfony\'s Request object or filter_input\\(\\) instead\\.$#', - 'count' => 13, + 'count' => 12, 'path' => __DIR__ . '/../../interface/patient_file/pos_checkout_ippf.php', ]; $ignoreErrors[] = [ @@ -3403,7 +3403,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Direct access to \\$_GET is forbidden\\. Use Symfony\'s Request object or filter_input\\(\\) instead\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_list.php', ]; $ignoreErrors[] = [ diff --git a/interface/billing/edi_271.php b/interface/billing/edi_271.php index 267daf2411f4..13c55080b894 100644 --- a/interface/billing/edi_271.php +++ b/interface/billing/edi_271.php @@ -38,7 +38,8 @@ $batch_log = ''; if (isset($_FILES) && !empty($_FILES)) { - $target = $target . time() . basename((string) $_FILES['uploaded']['name']); + $uploadedName = (string) $_FILES['uploaded']['name']; + $target = $target . time() . basename($uploadedName); if ($_FILES['uploaded']['size'] > 350000) { $message .= xlt('Your file is too large') . "
"; @@ -46,7 +47,8 @@ if (mime_content_type($_FILES['uploaded']['tmp_name']) != "text/plain") { $message .= xlt('You may only upload .txt files') . "
"; } - if (preg_match("/(.*)\.(inc|php|php7|php8)$/i", (string) $_FILES['uploaded']['name']) !== 0) { + $uploadedExt = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION)); + if (in_array($uploadedExt, ['inc', 'php', 'php7', 'php8'], true)) { $message .= xlt('Invalid file type.') . "
"; } if (!isset($message)) { @@ -56,7 +58,7 @@ $uploadedFile = $cryptoGen->encryptStandard($uploadedFile, keySource: KeySource::Database); } if (file_put_contents($target, $uploadedFile)) { - $message = xlt('The following EDI file has been uploaded') . ': "' . text(basename((string) $_FILES['uploaded']['name'])) . '"'; + $message = xlt('The following EDI file has been uploaded') . ': "' . text(basename($uploadedName)) . '"'; $Response271 = file_get_contents($target); if ($cryptoGen->cryptCheckStandard($Response271)) { $Response271 = $cryptoGen->decryptStandard($Response271, keySource: KeySource::Database); @@ -64,10 +66,10 @@ if ($Response271) { $batch_log = EDI270::parseEdi271($Response271); } else { - $message = xlt('The following EDI file upload failed to open') . ': "' . text(basename((string) $_FILES['uploaded']['name'])) . '"'; + $message = xlt('The following EDI file upload failed to open') . ': "' . text(basename($uploadedName)) . '"'; } } else { - $message = xlt('The following EDI file failed save to archive') . ': "' . text(basename((string) $_FILES['uploaded']['name'])) . '"'; + $message = xlt('The following EDI file failed save to archive') . ': "' . text(basename($uploadedName)) . '"'; } } else { $message .= xlt('Sorry, there was a problem uploading your file') . "

"; diff --git a/interface/fax/fax_dispatch.php b/interface/fax/fax_dispatch.php index b16e15dadd2f..a9bab5b55c15 100644 --- a/interface/fax/fax_dispatch.php +++ b/interface/fax/fax_dispatch.php @@ -51,7 +51,8 @@ die("No filename was given."); } -$ext = substr((string) $filename, strrpos((string) $filename, '.')); +$filename = (string) $filename; +$ext = substr($filename, strrpos($filename, '.')); $filebase = basename("/$filename", $ext); $faxcache = OEGlobalsBag::getInstance()->get('OE_SITE_DIR') . "/faxcache/$mode/$filebase"; @@ -127,10 +128,10 @@ function mergeTiffs() // if ($_POST['form_cb_copy_type'] == 1) { // Compute a target filename that does not yet exist. - $ffname = check_file_dir_name(trim((string) $_POST['form_filename'])); - $i = strrpos((string) $ffname, '.'); + $ffname = (string) check_file_dir_name(trim((string) $_POST['form_filename'])); + $i = strrpos($ffname, '.'); if ($i) { - $ffname = trim(substr((string) $ffname, 0, $i)); + $ffname = trim(substr($ffname, 0, $i)); } if (!$ffname) { @@ -356,7 +357,7 @@ function mergeTiffs() $form_cb_delete = '2'; while (false !== ($jfname = readdir($dh))) { - if (preg_match('/\.jpg$/', $jfname)) { + if (strtolower(pathinfo($jfname, PATHINFO_EXTENSION)) === 'jpg') { $form_cb_delete = '1'; } } diff --git a/interface/forms/vitals/growthchart/chart.php b/interface/forms/vitals/growthchart/chart.php index 499bb33a8e36..fb06775816d9 100644 --- a/interface/forms/vitals/growthchart/chart.php +++ b/interface/forms/vitals/growthchart/chart.php @@ -64,11 +64,13 @@ $isMetric = (((OEGlobalsBag::getInstance()->get('units_of_measurement') == 2) || (OEGlobalsBag::getInstance()->get('units_of_measurement') == 4)) ? true : false); $patient_data = ""; +$sex = ''; if (isset($pid) && is_numeric($pid)) { $patient_data = getPatientData($pid, "fname, lname, sex, DATE_FORMAT(DOB,'%Y%m%d') as DOB"); $nowAge = getPatientAge($patient_data['DOB']); $dob = $patient_data['DOB']; $name = $patient_data['fname'] . " " . $patient_data['lname']; + $sex = strtolower((string) $patient_data['sex']); } // The first data point in the DATA set is significant. It tells the date @@ -164,13 +166,13 @@ function convertWeightToUs($weight) $HT_x = 1187; //start here to draw wt and height graph at bottom of Head circumference chart $HT_delta_x = 24.32; - if (preg_match('/^male/i', (string) $patient_data['sex'])) { + if (str_starts_with($sex, 'male')) { $chart = "birth-24mos_boys_HC.png"; // added by BM for CSS html output $chartCss1 = "birth-24mos_boys_HC-1.png"; $chartCss2 = "birth-24mos_boys_HC-2.png"; - } elseif (preg_match('/^female/i', (string) $patient_data['sex'])) { + } elseif (str_starts_with($sex, 'female')) { $chart = "birth-24mos_girls_HC.png"; // added by BM for CSS html output @@ -216,13 +218,13 @@ function convertWeightToUs($weight) $bmi_dot_y = 1130; $bmi_delta_y = 37.15; - if (preg_match('/^male/i', (string) $patient_data['sex'])) { + if (str_starts_with($sex, 'male')) { $chart = "2-20yo_boys_BMI.png"; // added by BM for CSS html output $chartCss1 = "2-20yo_boys_BMI-1.png"; $chartCss2 = "2-20yo_boys_BMI-2.png"; - } elseif (preg_match('/^female/i', (string) $patient_data['sex'])) { + } elseif (str_starts_with($sex, 'female')) { $chart = "2-20yo_girls_BMI.png"; // added by BM for CSS html output diff --git a/interface/patient_file/letter.php b/interface/patient_file/letter.php index 98e85792170a..bddf534393a9 100644 --- a/interface/patient_file/letter.php +++ b/interface/patient_file/letter.php @@ -554,15 +554,7 @@ function insertAtCursor(myField, myValue) { continue; } - if (preg_match("/\.php$/", $tfname)) { - continue; - } - - if (preg_match("/\.jpg$/", $tfname)) { - continue; - } - - if (preg_match("/\.png$/", $tfname)) { + if (in_array(strtolower(pathinfo($tfname, PATHINFO_EXTENSION)), ['php', 'jpg', 'png'], true)) { continue; } diff --git a/interface/patient_file/pos_checkout_ippf.php b/interface/patient_file/pos_checkout_ippf.php index 1c15f3174b25..585d43228fe3 100644 --- a/interface/patient_file/pos_checkout_ippf.php +++ b/interface/patient_file/pos_checkout_ippf.php @@ -429,9 +429,10 @@ function receiptPaymentLineIppf($paydate, $amount, $description = '', $method = } echo " \n"; echo " " . text(oeFormatShortDate($paydate)) . "\n"; echo " " . text($refno) . "\n"; @@ -1638,10 +1639,11 @@ function write_old_payment_line($pay_type, $date, $method, $reference, $amount): // Post discount. if ($_POST['form_discount'] != 0) { + $discount = trim((string) $_POST['form_discount']); if (OEGlobalsBag::getInstance()->getBoolean('discount_by_money')) { - $amount = formatMoneyNumber(trim((string) $_POST['form_discount'])); + $amount = formatMoneyNumber($discount); } else { - $amount = formatMoneyNumber(trim((string) $_POST['form_discount']) * $form_amount / 100); + $amount = formatMoneyNumber($discount * $form_amount / 100); } $memo = trimPost('form_discount_type'); $recorder = new Recorder(); @@ -2006,9 +2008,10 @@ function adjTypeFromCustomer(customer) { "list_id = 'chargecats' AND activity = 1" ); while ($tmprow = sqlFetchArray($tmpres)) { + $notes = (string) $tmprow['notes']; if ( - preg_match('/ADJ=(\w+)/', (string) $tmprow['notes'], $matches) || - preg_match('/ADJ="(.*?)"/', (string) $tmprow['notes'], $matches) + preg_match('/ADJ=(\w+)/', $notes, $matches) || + preg_match('/ADJ="(.*?)"/', $notes, $matches) ) { echo " if (customer == " . js_escape($tmprow['option_id']) . ") ret = " . js_escape($matches[1]) . ";\n"; } @@ -2390,13 +2393,13 @@ function validate() { if ($codetype !== 'IPPF2') { continue; } - if (preg_match('/^211/', $code)) { + if (str_starts_with($code, '211')) { $gcac_related_visit = true; if ( - preg_match('/^211313030110/', $code) // Medical - || preg_match('/^211323030230/', $code) // Surgical - || preg_match('/^211403030110/', $code) // Incomplete Medical - || preg_match('/^211403030230/', $code) // Incomplete Surgical + str_starts_with($code, '211313030110') // Medical + || str_starts_with($code, '211323030230') // Surgical + || str_starts_with($code, '211403030110') // Incomplete Medical + || str_starts_with($code, '211403030230') // Incomplete Surgical ) { $gcac_service_provided = true; } diff --git a/interface/patient_file/pos_checkout_normal.php b/interface/patient_file/pos_checkout_normal.php index 5300129be8a0..9d722beb4298 100644 --- a/interface/patient_file/pos_checkout_normal.php +++ b/interface/patient_file/pos_checkout_normal.php @@ -1046,7 +1046,7 @@ function computeTotals() { if ($codetype !== 'IPPF') { continue; } - if (preg_match('/^25222/', $code)) { + if (str_starts_with($code, '25222')) { $gcac_related_visit = true; if (preg_match('/^25222[34]/', $code)) { $gcac_service_provided = true; diff --git a/interface/reports/appt_encounter_report.php b/interface/reports/appt_encounter_report.php index b373d6964845..56db9c2ea2dd 100644 --- a/interface/reports/appt_encounter_report.php +++ b/interface/reports/appt_encounter_report.php @@ -426,7 +426,7 @@ function endDoctor(&$docrow): void continue; } - if (preg_match('/^25222/', $code)) { + if (str_starts_with($code, '25222')) { $gcac_related_visit = true; } } diff --git a/interface/reports/ippf_statistics.php b/interface/reports/ippf_statistics.php index 25d13408315b..7435fa3d8ca0 100644 --- a/interface/reports/ippf_statistics.php +++ b/interface/reports/ippf_statistics.php @@ -226,36 +226,36 @@ function ippf_stats_genNumCell($num, $cnum): void // Translate an IPPF code to the corresponding descriptive name of its // contraceptive method, or to an empty string if none applies. // -function getContraceptiveMethod($code) +function getContraceptiveMethod(string $code) { $key = ''; - if (preg_match('/^111101/', (string) $code)) { + if (str_starts_with($code, '111101')) { $key = xl('Pills'); - } elseif (preg_match('/^11111[1-9]/', (string) $code)) { + } elseif (preg_match('/^11111[1-9]/', $code)) { $key = xl('Injectables'); - } elseif (preg_match('/^11112[1-9]/', (string) $code)) { + } elseif (preg_match('/^11112[1-9]/', $code)) { $key = xl('Implants'); - } elseif (preg_match('/^111132/', (string) $code)) { + } elseif (str_starts_with($code, '111132')) { $key = xl('Patch'); - } elseif (preg_match('/^111133/', (string) $code)) { + } elseif (str_starts_with($code, '111133')) { $key = xl('Vaginal Ring'); - } elseif (preg_match('/^112141/', (string) $code)) { + } elseif (str_starts_with($code, '112141')) { $key = xl('Male Condoms'); - } elseif (preg_match('/^112142/', (string) $code)) { + } elseif (str_starts_with($code, '112142')) { $key = xl('Female Condoms'); - } elseif (preg_match('/^11215[1-9]/', (string) $code)) { + } elseif (preg_match('/^11215[1-9]/', $code)) { $key = xl('Diaphragms/Caps'); - } elseif (preg_match('/^11216[1-9]/', (string) $code)) { + } elseif (preg_match('/^11216[1-9]/', $code)) { $key = xl('Spermicides'); - } elseif (preg_match('/^11317[1-9]/', (string) $code)) { + } elseif (preg_match('/^11317[1-9]/', $code)) { $key = xl('IUD'); - } elseif (preg_match('/^145212/', (string) $code)) { + } elseif (str_starts_with($code, '145212')) { $key = xl('Emergency Contraception'); - } elseif (preg_match('/^121181.13/', (string) $code)) { + } elseif (preg_match('/^121181.13/', $code)) { $key = xl('Female VSC'); - } elseif (preg_match('/^122182.13/', (string) $code)) { + } elseif (preg_match('/^122182.13/', $code)) { $key = xl('Male VSC'); - } elseif (preg_match('/^131191.10/', (string) $code)) { + } elseif (preg_match('/^131191.10/', $code)) { $key = xl('Awareness-Based'); } @@ -321,17 +321,17 @@ function getRelatedAbortionMethod($row) // Translate an IPPF code to the corresponding descriptive name of its // abortion method, or to an empty string if none applies. // -function getAbortionMethod($code) +function getAbortionMethod(string $code) { $key = ''; - if (preg_match('/^25222[34]/', (string) $code)) { - if (preg_match('/^2522231/', (string) $code)) { + if (preg_match('/^25222[34]/', $code)) { + if (str_starts_with($code, '2522231')) { $key = xl('D&C'); - } elseif (preg_match('/^2522232/', (string) $code)) { + } elseif (str_starts_with($code, '2522232')) { $key = xl('D&E'); - } elseif (preg_match('/^2522233/', (string) $code)) { + } elseif (str_starts_with($code, '2522233')) { $key = xl('MVA'); - } elseif (preg_match('/^252224/', (string) $code)) { + } elseif (str_starts_with($code, '252224')) { $key = xl('Medical'); } else { $key = xl('Other Surgical'); @@ -576,7 +576,7 @@ function ippfLoadColumnData(string $key, array $row, int $quantity = 1): void // This is called for each IPPF service code that is selected. // -function process_ippf_code($row, $code, $quantity = 1): void +function process_ippf_code($row, string $code, $quantity = 1): void { global $areport, $arr_titles, $form_by, $form_content; @@ -585,9 +585,9 @@ function process_ippf_code($row, $code, $quantity = 1): void // SRH including Family Planning // if ($form_by === '1') { - if (preg_match('/^1/', (string) $code)) { + if (str_starts_with($code, '1')) { $key = xl('SRH - Family Planning'); - } elseif (preg_match('/^2/', (string) $code)) { + } elseif (str_starts_with($code, '2')) { $key = xl('SRH Non Family Planning'); } else { if ($form_content != 5) { @@ -595,33 +595,33 @@ function process_ippf_code($row, $code, $quantity = 1): void } } } elseif ($form_by === '3') { // General Service Category - if (preg_match('/^1/', (string) $code)) { + if (str_starts_with($code, '1')) { $key = xl('SRH - Family Planning'); - } elseif (preg_match('/^2/', (string) $code)) { + } elseif (str_starts_with($code, '2')) { $key = xl('SRH Non Family Planning'); - } elseif (preg_match('/^3/', (string) $code)) { + } elseif (str_starts_with($code, '3')) { $key = xl('Non-SRH Medical'); - } elseif (preg_match('/^4/', (string) $code)) { + } elseif (str_starts_with($code, '4')) { $key = xl('Non-SRH Non-Medical'); } else { $key = xl('Invalid Service Codes'); } } elseif ($form_by === '13') { // Abortion-Related Category - if (preg_match('/^252221/', (string) $code)) { + if (str_starts_with($code, '252221')) { $key = xl('Pre-Abortion Counseling'); - } elseif (preg_match('/^252222/', (string) $code)) { + } elseif (str_starts_with($code, '252222')) { $key = xl('Pre-Abortion Consultation'); - } elseif (preg_match('/^252223/', (string) $code)) { + } elseif (str_starts_with($code, '252223')) { $key = xl('Induced Abortion'); - } elseif (preg_match('/^252224/', (string) $code)) { + } elseif (str_starts_with($code, '252224')) { $key = xl('Medical Abortion'); - } elseif (preg_match('/^252225/', (string) $code)) { + } elseif (str_starts_with($code, '252225')) { $key = xl('Incomplete Abortion Treatment'); - } elseif (preg_match('/^252226/', (string) $code)) { + } elseif (str_starts_with($code, '252226')) { $key = xl('Post-Abortion Care'); - } elseif (preg_match('/^252227/', (string) $code)) { + } elseif (str_starts_with($code, '252227')) { $key = xl('Post-Abortion Counseling'); - } elseif (preg_match('/^25222/', (string) $code)) { + } elseif (str_starts_with($code, '25222')) { $key = xl('Other/Generic Abortion-Related'); } else { if ($form_content != 5) { @@ -714,7 +714,7 @@ function process_ippf_code($row, $code, $quantity = 1): void } elseif ($form_by === '8') { // Post-Abortion Care and Followup by Source. // Requirements just call for counting sessions, but this way the columns // can be anything - age category, religion, whatever. - if (preg_match('/^25222[567]/', (string) $code)) { // care, followup and incomplete abortion treatment + if (preg_match('/^25222[567]/', $code)) { // care, followup and incomplete abortion treatment $key = getGcacClientStatus($row); } else { return; @@ -761,7 +761,7 @@ function process_ippf_code($row, $code, $quantity = 1): void // Decided not to have the abortion // } elseif ($form_by === '12') { - if (preg_match('/^252221/', (string) $code)) { // all pre-abortion counseling + if (str_starts_with($code, '252221')) { // all pre-abortion counseling $key = getGcacClientStatus($row); } else { return; diff --git a/interface/super/edit_list.php b/interface/super/edit_list.php index 4840ed430f2b..8074837f0088 100644 --- a/interface/super/edit_list.php +++ b/interface/super/edit_list.php @@ -43,7 +43,7 @@ $list_id = 'language'; $blank_list_id = true; } else { - $list_id = $_REQUEST['list_id']; + $list_id = (string) $_REQUEST['list_id']; } // Check authorization. @@ -242,7 +242,7 @@ function listChecksum($list_id) $notes = trim($iter['notes'] ?? ''); } - if (preg_match("/Eye_QP_/", (string) $list_id)) { + if (preg_match("/Eye_QP_/", $list_id)) { if (preg_match("/^[BLR]/", $id)) { $stuff = explode("_", $id)[0]; $iter['mapping'] = substr($stuff, 1); @@ -563,7 +563,7 @@ function writeOptionLine($option_id, string $title, $seq, $default, $value, $map attr($codes) . "' onclick='select_clin_term_code(this)' size='25' maxlength='255' class='optin form-control form-control-sm' />"; echo "\n"; - if (preg_match('/_issue_list$/', (string) $list_id)) { + if (str_ends_with((string) $list_id, '_issue_list')) { echo " "; echo generate_select_list("opt[$opt_line_no][subtype]", 'issue_subtypes', $subtype, 'Subtype', ' ', 'optin'); echo "\n"; @@ -1157,8 +1157,8 @@ function listSelected() { * Keep proper list name (otherwise list name changes according to * the options shown on the screen). */ - $list_id_container = $_GET["list_id_container"] ?? null; - if (isset($_GET["list_id_container"]) && strlen((string) $list_id_container) > 0) { + $list_id_container = (string) ($_GET["list_id_container"] ?? ''); + if ($list_id_container !== '') { $list_id = $list_id_container; } @@ -1381,7 +1381,7 @@ function lister() { + if (str_ends_with((string) $list_id, '_issue_list')) { ?> $value) { - if ($value == null && preg_match("/_flag$/", (string) $key)) { + if ($value == null && str_ends_with((string) $key, '_flag')) { // use default setting $mergedPlan[$key] = $plan[$key]; } else { @@ -2205,7 +2205,7 @@ function resolve_rules_sql($type = '', $patient_id = '0', $configurableOnly = fa // note this explicitly relies on type conversion, null, "", 0 becoming equal to null... // if anything changes in language spec this might break. // TODO: consider using strict comparison - if ($value == null && preg_match("/_flag$/", (string) $key)) { + if ($value == null && str_ends_with((string) $key, '_flag')) { // use default setting $mergedRule[$key] = $rule[$key]; } else { diff --git a/library/options.inc.php b/library/options.inc.php index ba838237ce19..19a30fb085cc 100644 --- a/library/options.inc.php +++ b/library/options.inc.php @@ -692,7 +692,7 @@ function generate_form_field($frow, $currvalue): void if ($data_type == 46) { // support for single-selection list with comment support $selectedValues = explode("|", (string) $currvalue); - if (!preg_match('/^comment_/', (string) $currvalue) || (count($selectedValues) == 1)) { + if (!str_starts_with((string) $currvalue, 'comment_') || (count($selectedValues) == 1)) { $display = "display:none"; $comment = ""; } else { @@ -4288,7 +4288,7 @@ function get_layout_form_value($frow, $prefix = 'form_') } } elseif ($data_type == 46) { $reslist = trim((string) $_POST["$prefix$field_id"]); - if (preg_match('/^comment_/', $reslist)) { + if (str_starts_with($reslist, 'comment_')) { $res_comment = str_replace('|', ' ', $_POST["{$prefix}text_$field_id"]); $value = $reslist . "|" . $res_comment; } else { diff --git a/portal/home.php b/portal/home.php index 18ffb6868758..91298470e926 100644 --- a/portal/home.php +++ b/portal/home.php @@ -118,10 +118,11 @@ foreach ($appts as $row) { $status_title = getListItemTitle('apptstat', $row['pc_apptstatus']); $count++; + $startTime = (string) $row['pc_startTime']; $dayname = xl(date('l', strtotime((string) $row['pc_eventDate']))); $dispampm = 'am'; - $disphour = (int)substr((string) $row['pc_startTime'], 0, 2); - $dispmin = substr((string) $row['pc_startTime'], 3, 2); + $disphour = (int)substr($startTime, 0, 2); + $dispmin = substr($startTime, 3, 2); if ($disphour >= 12) { $dispampm = 'pm'; if ($disphour > 12) { @@ -152,10 +153,11 @@ foreach ($past_appts as $row) { $status_title = getListItemTitle('apptstat', $row['pc_apptstatus']); $pastCount++; + $startTime = (string) $row['pc_startTime']; $dayname = xl(date('l', strtotime((string) $row['pc_eventDate']))); $dispampm = 'am'; - $disphour = (int)substr((string) $row['pc_startTime'], 0, 2); - $dispmin = substr((string) $row['pc_startTime'], 3, 2); + $disphour = (int)substr($startTime, 0, 2); + $dispmin = substr($startTime, 3, 2); if ($disphour >= 12) { $dispampm = 'pm'; if ($disphour > 12) { @@ -190,7 +192,8 @@ function collectStyles(): array if ( $tfname == 'style_blue.css' || $tfname == 'style_pdf.css' || - !preg_match("/^" . 'style_' . ".*\.css$/", $tfname) + !str_starts_with($tfname, 'style_') || + !str_ends_with($tfname, '.css') ) { continue; } diff --git a/portal/import_template.php b/portal/import_template.php index 5abae0b3b4ca..bb0125d2d1e8 100644 --- a/portal/import_template.php +++ b/portal/import_template.php @@ -232,10 +232,10 @@ } // parse out what we need $name = preg_replace("/[^A-Z0-9.]/i", " ", (string)$_FILES['template_files']['name'][$i]); - if (preg_match("/(.*)\.(php|php7|php8|doc|docx)$/i", (string)$name) !== 0) { + $parts = pathinfo((string)$name); + if (in_array(strtolower($parts['extension'] ?? ''), ['php', 'php7', 'php8', 'doc', 'docx'], true)) { die(xlt('Invalid file type.')); } - $parts = pathinfo((string)$name); $name = ucwords(strtolower($parts["filename"])); if (empty($patient)) { $patient = ['-1']; diff --git a/portal/patient/fwk/libs/util/parsecsv.lib.php b/portal/patient/fwk/libs/util/parsecsv.lib.php index c1e6b0013223..fd10198807f4 100644 --- a/portal/patient/fwk/libs/util/parsecsv.lib.php +++ b/portal/patient/fwk/libs/util/parsecsv.lib.php @@ -254,7 +254,7 @@ function save($file = null, $data = [], $append = false, $fields = []) } $mode = ($append) ? 'at' : 'wt'; - $is_php = (preg_match('/\.php$/i', (string) $file)) ? true : false; + $is_php = strtolower(pathinfo((string) $file, PATHINFO_EXTENSION)) === 'php'; return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); } @@ -681,7 +681,7 @@ function load_data($input = null) $this->file = $file; } - if (preg_match('/\.php$/i', (string) $file) && preg_match('/<\?.*?\?>(.*)/ims', (string) $data, $strip)) { + if (strtolower(pathinfo((string) $file, PATHINFO_EXTENSION)) === 'php' && preg_match('/<\?.*?\?>(.*)/ims', (string) $data, $strip)) { $data = ltrim($strip [1]); } diff --git a/src/Services/Utils/SQLUpgradeService.php b/src/Services/Utils/SQLUpgradeService.php index e76dd685e7ce..6cd1de1c66c1 100644 --- a/src/Services/Utils/SQLUpgradeService.php +++ b/src/Services/Utils/SQLUpgradeService.php @@ -466,7 +466,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfNotMigrateClickOptions/', $line)) { + } elseif (str_starts_with($line, '#IfNotMigrateClickOptions')) { if ($this->tableExists("issue_types")) { $skipping = true; } else { @@ -478,7 +478,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfNotListOccupation/', $line)) { + } elseif (str_starts_with($line, '#IfNotListOccupation')) { if (($this->listExists("Occupation")) || (!$this->columnExists('patient_data', 'occupation'))) { $skipping = true; } else { @@ -491,7 +491,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfNotListReaction/', $line)) { + } elseif (str_starts_with($line, '#IfNotListReaction')) { if (($this->listExists("reaction")) || (!$this->columnExists('lists', 'reaction'))) { $skipping = true; } else { @@ -504,7 +504,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfNotListImmunizationManufacturer/', $line)) { + } elseif (str_starts_with($line, '#IfNotListImmunizationManufacturer')) { if ($this->listExists("Immunization_Manufacturer")) { $skipping = true; } else { @@ -517,7 +517,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfNotWenoRx/', $line)) { + } elseif (str_starts_with($line, '#IfNotWenoRx')) { if ($this->tableHasRow('erx_weno_drugs', "drug_id", '1008') == true) { $skipping = true; } else { @@ -530,7 +530,7 @@ function upgradeFromSqlFile($filename, $path = '') $this->echo("

$skip_msg $line

\n"); } // convert all *text types to use default null setting - } elseif (preg_match('/^#IfTextNullFixNeeded/', $line)) { + } elseif (str_starts_with($line, '#IfTextNullFixNeeded')) { $items_to_convert = sqlStatement( "SELECT col.`table_name` AS table_name, col.`column_name` AS column_name, col.`data_type` AS data_type, col.`column_comment` AS column_comment @@ -575,7 +575,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfInnoDBMigrationNeeded/', $line)) { + } elseif (str_starts_with($line, '#IfInnoDBMigrationNeeded')) { // find MyISAM tables and attempt to convert them //tables that need to skip InnoDB migration (stay at MyISAM for now) $tables_skip_migration = ['form_eye_mag']; @@ -610,7 +610,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#ConvertLayoutProperties/', $line)) { + } elseif (str_starts_with($line, '#ConvertLayoutProperties')) { if ($skipping) { $this->echo("

$skip_msg $line

\n"); } else { @@ -618,7 +618,7 @@ function upgradeFromSqlFile($filename, $path = '') $this->flush_echo(); $this->convertLayoutProperties(); } - } elseif (preg_match('/^#IfDocumentNamingNeeded/', $line)) { + } elseif (str_starts_with($line, '#IfDocumentNamingNeeded')) { $emptyNames = sqlStatementNoLog("SELECT `id`, `url`, `name`, `couch_docid` FROM `documents` WHERE `name` = '' OR `name` IS NULL"); if (sqlNumRows($emptyNames) > 0) { $this->echo("

Converting document names.

\n"); @@ -644,7 +644,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfVitalsDatesNeeded/', $line)) { + } elseif (str_starts_with($line, '#IfVitalsDatesNeeded')) { $emptyDates = sqlStatementNoLog("SELECT fv.id as vitals_id, f.date as new_date FROM form_vitals fv LEFT JOIN forms f on fv.id = f.form_id WHERE fv.date = '0000-00-00 00:00:00' AND f.form_name = 'Vitals'"); if (sqlNumRows($emptyDates) > 0) { $this->echo("

Converting empty vital dates.

\n"); @@ -661,7 +661,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfMBOEncounterNeeded/', $line)) { + } elseif (str_starts_with($line, '#IfMBOEncounterNeeded')) { $emptyMBOEncounters = sqlStatementNoLog("SELECT `pid` FROM `form_misc_billing_options` WHERE `encounter` IS NULL"); if (sqlNumRows($emptyMBOEncounters) > 0) { $this->echo("

Linking encounters to misc billing options forms.

\n"); @@ -703,7 +703,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfEyeFormLaserCategoriesNeeded/', $line)) { + } elseif (str_starts_with($line, '#IfEyeFormLaserCategoriesNeeded')) { $eyeFormCategoryParent = sqlQueryNoLog("SELECT `id`, `rght` FROM `categories` WHERE `name` = 'Eye Module'"); $eyeFormAntSegLaser = sqlQueryNoLog("SELECT `id` FROM `categories` WHERE `name` = 'AntSeg Laser - Eye'"); if (!empty($eyeFormCategoryParent) && empty($eyeFormAntSegLaser)) { @@ -748,7 +748,7 @@ function upgradeFromSqlFile($filename, $path = '') if ($skipping) { $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#IfCareTeamsV1MigrationNeeded/', $line)) { + } elseif (str_starts_with($line, '#IfCareTeamsV1MigrationNeeded')) { $sql = "SELECT COLUMN_COMMENT = 'Deprecated field, use care_team_member table instead' AS is_migrated FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() @@ -762,14 +762,14 @@ function upgradeFromSqlFile($filename, $path = '') $skipping = true; $this->echo("

$skip_msg $line

\n"); } - } elseif (preg_match('/^#EndIf/', $line)) { + } elseif (str_starts_with($line, '#EndIf')) { $skipping = false; } - if (preg_match('/^#SpecialSql/', $line)) { + if (str_starts_with($line, '#SpecialSql')) { $special = true; $line = " "; - } elseif (preg_match('/^#EndSpecialSql/', $line)) { + } elseif (str_starts_with($line, '#EndSpecialSql')) { $special = false; $trim = false; $line = " "; @@ -1112,11 +1112,11 @@ private function clickOptionsMigrate() $this->echo("Importing clickoption setting
"); while (!feof($file_handle)) { $line_of_text = fgets($file_handle); - if (preg_match('/^#/', $line_of_text)) { + if ($line_of_text === false || $line_of_text === "") { continue; } - if ($line_of_text == "") { + if (str_starts_with($line_of_text, '#')) { continue; } @@ -1289,7 +1289,7 @@ private function convertLayoutProperties() { $res = sqlStatement("SELECT DISTINCT form_id FROM layout_options ORDER BY form_id"); while ($row = sqlFetchArray($res)) { - $form_id = $row['form_id']; + $form_id = (string)$row['form_id']; $props = [ 'title' => 'Unknown', 'mapping' => 'Core', @@ -1297,7 +1297,7 @@ private function convertLayoutProperties() 'activity' => '1', 'option_value' => '0', ]; - if (str_starts_with((string)$form_id, 'LBF')) { + if (str_starts_with($form_id, 'LBF')) { $props = sqlQuery( "SELECT title, mapping, notes, activity, option_value FROM list_options WHERE list_id = 'lbfnames' AND option_id = ?", [$form_id] @@ -1308,7 +1308,7 @@ private function convertLayoutProperties() if (empty($props['mapping'])) { $props['mapping'] = 'Clinical'; } - } elseif (str_starts_with((string)$form_id, 'LBT')) { + } elseif (str_starts_with($form_id, 'LBT')) { $props = sqlQuery( "SELECT title, mapping, notes, activity, option_value FROM list_options WHERE list_id = 'transactions' AND option_id = ?", [$form_id] From 7ab6796a84a51e224d2c96f5792cfba28cdacc7b Mon Sep 17 00:00:00 2001 From: "Michael A. Smith" Date: Tue, 28 Apr 2026 17:19:03 -0400 Subject: [PATCH 24/82] =?UTF-8?q?chore(phpstan):=20drain=20variable.undefi?= =?UTF-8?q?ned=20baseline=20(3064=20=E2=86=92=202927)=20(#11887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drains 137 entries from the `variable.undefined.php` PHPStan baseline (#11792, Phase 5). Scope expanded beyond the original `interface/batchcom` to include: - `interface/batchcom` - `interface/therapy_groups` - `interface/usergroup` - `interface/super` - `interface/orders` ## Fix patterns applied - Hoist init of array accumulators / scalars before conditional blocks - Use `?? ''` / `??=` defaults at use sites (template-style includes) - Replace legacy globals (`$srcdir`, `$webroot`, `$webserver_root`, `$rootdir`, `$OE_SITE_DIR`) with `OEGlobalsBag::getInstance()` typed getters - Replace stale `$pid` reads with `SessionWrapperFactory::getInstance()->getActiveSession()->get('pid')` - File-level `@var` annotations for globals provided by required files (gacl/admin precedent #11826) - Convert `for ($i = 0, $j = $k; ...; ...)` comma-init loops to explicit `while` (PHPStan can't follow comma expressions in for-init) ## Real bug fix `interface/super/layout_service_codes.php` was calling `fclose($eres)` on the wrong handle. Should be `fclose($fhcsv)`. ## Follow-up Filed #11892 to track replacing the manual loop-counter pattern (`$i = 0; while (...) { ...; ++$i; }`) with `foreach` indices and `\Generator`s. --- .phpstan/baseline/argument.type.php | 103 +-- .phpstan/baseline/assignOp.invalid.php | 30 - .phpstan/baseline/binaryOp.invalid.php | 37 +- .phpstan/baseline/cast.string.php | 7 +- .phpstan/baseline/echo.nonString.php | 27 +- .../baseline/encapsedStringPart.nonString.php | 170 ----- .phpstan/baseline/foreach.nonIterable.php | 21 +- .../baseline/function.alreadyNarrowedType.php | 5 + .phpstan/baseline/function.impossibleType.php | 5 + .phpstan/baseline/isset.variable.php | 10 + .phpstan/baseline/nullCoalesce.variable.php | 10 + .../baseline/offsetAccess.invalidOffset.php | 20 +- .phpstan/baseline/offsetAccess.nonArray.php | 5 - .../offsetAccess.nonOffsetAccessible.php | 443 +---------- .phpstan/baseline/postInc.type.php | 5 - .phpstan/baseline/variable.undefined.php | 685 ------------------ .phpstan/fatal-baseline-caps.php | 2 +- interface/batchcom/batchEmail.php | 12 +- interface/batchcom/batchPhoneList.php | 7 + interface/batchcom/batch_reminders.php | 2 + interface/batchcom/batchcom.inc.php | 4 +- interface/batchcom/batchcom.php | 6 +- interface/batchcom/emailnotification.php | 9 +- interface/batchcom/settingsnotification.php | 10 +- interface/batchcom/smsnotification.php | 8 +- interface/orders/find_order_popup.php | 8 +- interface/orders/gen_hl7_order.inc.php | 2 +- interface/orders/list_reports.php | 11 +- interface/orders/load_compendium.php | 1 + interface/orders/order_manifest.php | 4 +- interface/orders/orders_results.php | 10 +- interface/orders/patient_match_dialog.php | 4 +- interface/orders/pending_orders.php | 4 +- interface/orders/procedure_provider_edit.php | 4 +- interface/orders/procedure_provider_list.php | 3 +- interface/orders/procedure_stats.php | 2 +- interface/orders/receive_hl7_results.inc.php | 4 +- interface/orders/types.php | 3 +- interface/orders/types_ajax.php | 5 +- interface/orders/types_edit.php | 3 +- interface/super/edit_globals.php | 12 +- interface/super/edit_layout.php | 6 +- interface/super/edit_layout_props.php | 2 +- interface/super/edit_list.php | 8 +- interface/super/layout_service_codes.php | 5 +- interface/super/manage_document_templates.php | 2 +- interface/super/manage_site_files.php | 2 +- .../templates/field_html_display_section.php | 1 + .../field_multi_sorted_list_selector.php | 1 + interface/therapy_groups/index.php | 2 +- .../therapy_groups_encounters_model.php | 1 + .../therapy_groups_models/users_model.php | 1 + .../therapy_groups_views/addGroup.php | 4 + .../appointmentComponent.php | 3 + .../groupDetailsGeneralData.php | 6 + .../groupDetailsParticipants.php | 9 + .../therapy_groups_views/listGroups.php | 5 + interface/usergroup/addrbook_edit.php | 4 +- interface/usergroup/addrbook_list.php | 2 +- interface/usergroup/facilities.php | 2 +- interface/usergroup/facilities_add.php | 2 +- interface/usergroup/facility_admin.php | 2 +- interface/usergroup/facility_user.php | 2 +- interface/usergroup/facility_user_admin.php | 4 +- interface/usergroup/mfa_registrations.php | 2 +- interface/usergroup/mfa_totp.php | 4 +- interface/usergroup/mfa_u2f.php | 2 +- .../usergroup/ssl_certificates_admin.php | 2 + interface/usergroup/user_admin.php | 8 +- interface/usergroup/user_info.php | 4 +- interface/usergroup/usergroup_admin.php | 20 +- interface/usergroup/usergroup_admin_add.php | 13 +- 72 files changed, 327 insertions(+), 1527 deletions(-) diff --git a/.phpstan/baseline/argument.type.php b/.phpstan/baseline/argument.type.php index 68cd1bec4aa2..72167b4dbdd4 100644 --- a/.phpstan/baseline/argument.type.php +++ b/.phpstan/baseline/argument.type.php @@ -1543,7 +1543,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/batchcom/batchEmail.php', ]; $ignoreErrors[] = [ @@ -1606,11 +1606,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr expects string, mixed given\\.$#', 'count' => 4, @@ -1618,7 +1613,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', ]; $ignoreErrors[] = [ @@ -1628,7 +1623,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', ]; $ignoreErrors[] = [ @@ -1638,7 +1633,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', ]; $ignoreErrors[] = [ @@ -13578,7 +13573,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function js_escape expects string, mixed given\\.$#', - 'count' => 10, + 'count' => 7, 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', ]; $ignoreErrors[] = [ @@ -14262,7 +14257,7 @@ 'path' => __DIR__ . '/../../interface/orders/types_ajax.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function text expects string, \\(float\\|int\\) given\\.$#', + 'message' => '#^Parameter \\#1 \\$text of function text expects string, int\\<1, max\\> given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/types_ajax.php', ]; @@ -14278,7 +14273,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr expects string, mixed given\\.$#', - 'count' => 11, + 'count' => 10, 'path' => __DIR__ . '/../../interface/orders/types_edit.php', ]; $ignoreErrors[] = [ @@ -19761,21 +19756,11 @@ 'count' => 4, 'path' => __DIR__ . '/../../interface/super/edit_globals.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$haystack of function in_array expects array, mixed given\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#3 \\$user of function setUserSetting expects int\\|null, mixed given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_globals.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$array of function natsort expects array, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$character of function ord expects string, mixed given\\.$#', 'count' => 1, @@ -19828,7 +19813,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr expects string, mixed given\\.$#', - 'count' => 36, + 'count' => 35, 'path' => __DIR__ . '/../../interface/super/edit_layout.php', ]; $ignoreErrors[] = [ @@ -19841,14 +19826,19 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_layout.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function js_escape expects string, int given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/super/edit_layout.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function js_escape expects string, mixed given\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../interface/super/edit_layout.php', ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 17, + 'count' => 16, 'path' => __DIR__ . '/../../interface/super/edit_layout.php', ]; $ignoreErrors[] = [ @@ -19966,14 +19956,9 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$stream of function fclose expects resource, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', ]; $ignoreErrors[] = [ @@ -20158,7 +20143,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 4, + 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', ]; $ignoreErrors[] = [ @@ -20207,13 +20192,18 @@ 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$key of function xlt expects literal\\-string, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$key of function xlt expects literal\\-string, array\\ given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function attr expects string, int given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr expects string, mixed given\\.$#', - 'count' => 8, + 'count' => 7, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; $ignoreErrors[] = [ @@ -20233,7 +20223,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 6, + 'count' => 3, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; $ignoreErrors[] = [ @@ -20256,9 +20246,14 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function attr expects string, int\\|string given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr expects string, mixed given\\.$#', - 'count' => 10, + 'count' => 9, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; $ignoreErrors[] = [ @@ -20266,9 +20261,14 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function attr_url expects string, int\\|string given\\.$#', + 'count' => 6, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr_url expects string, mixed given\\.$#', - 'count' => 7, + 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; $ignoreErrors[] = [ @@ -20277,13 +20277,13 @@ 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function js_url expects string, mixed given\\.$#', + 'message' => '#^Parameter \\#1 \\$text of function js_url expects string, int\\|string given\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 6, + 'count' => 4, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; $ignoreErrors[] = [ @@ -20296,14 +20296,24 @@ 'count' => 3, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$key of function xlt expects literal\\-string, array\\ given\\.$#', + 'count' => 3, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$key of function xlt expects literal\\-string, mixed given\\.$#', - 'count' => 5, + 'count' => 2, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$text of function attr expects string, array\\ given\\.$#', + 'count' => 2, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function attr expects string, mixed given\\.$#', - 'count' => 4, + 'count' => 2, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', ]; $ignoreErrors[] = [ @@ -20393,7 +20403,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/facilities.php', ]; $ignoreErrors[] = [ @@ -20576,11 +20586,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/user_info.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$text of function js_escape expects string, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_info.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', 'count' => 3, @@ -20628,7 +20633,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 10, + 'count' => 9, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; $ignoreErrors[] = [ @@ -20683,7 +20688,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$text of function text expects string, mixed given\\.$#', - 'count' => 8, + 'count' => 7, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/assignOp.invalid.php b/.phpstan/baseline/assignOp.invalid.php index 63d89fa6a92e..9dfc66340090 100644 --- a/.phpstan/baseline/assignOp.invalid.php +++ b/.phpstan/baseline/assignOp.invalid.php @@ -106,26 +106,6 @@ 'count' => 4, 'path' => __DIR__ . '/../../gacl/profiler.inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\.\\=" between mixed and string results in an error\\.$#', - 'count' => 10, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\.\\=" between mixed and non\\-falsy\\-string results in an error\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\.\\=" between mixed and non\\-falsy\\-string results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\.\\=" between mixed and non\\-falsy\\-string results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\.\\=" between non\\-falsy\\-string and mixed results in an error\\.$#', 'count' => 1, @@ -1261,21 +1241,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\.\\=" between mixed and non\\-falsy\\-string results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\.\\=" between string and mixed results in an error\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\.\\=" between mixed and non\\-falsy\\-string results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\.\\=" between string and mixed results in an error\\.$#', 'count' => 2, diff --git a/.phpstan/baseline/binaryOp.invalid.php b/.phpstan/baseline/binaryOp.invalid.php index 326380330fd0..80fdaecc8bc9 100644 --- a/.phpstan/baseline/binaryOp.invalid.php +++ b/.phpstan/baseline/binaryOp.invalid.php @@ -6081,26 +6081,11 @@ 'count' => 3, 'path' => __DIR__ . '/../../interface/orders/types.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\*" between mixed and 10 results in an error\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/orders/types_ajax.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\*" between mixed and 9 results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/types_ajax.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\+" between mixed and 0 results in an error\\.$#', 'count' => 6, 'path' => __DIR__ . '/../../interface/orders/types_ajax.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\+" between mixed and 1 results in an error\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/types_ajax.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\+" between mixed and 0 results in an error\\.$#', 'count' => 2, @@ -8306,11 +8291,6 @@ 'count' => 6, 'path' => __DIR__ . '/../../interface/reports/unique_seen_patients_report.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between \'global\\:\' and mixed results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between mixed and \' \\- \' results in an error\\.$#', 'count' => 1, @@ -8381,11 +8361,21 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between mixed and \'/documents…\' results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/super/manage_document_templates.php', +]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\+" between mixed and 1 results in an error\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/super/manage_site_files.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between mixed and \'/documents/education\' results in an error\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/super/manage_site_files.php', +]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between mixed and \' \' results in an error\\.$#', 'count' => 1, @@ -8471,11 +8461,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\+" between mixed and 1 results in an error\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; $ignoreErrors[] = [ 'message' => '#^Binary operation "\\." between mixed and \'\\?method…\' results in an error\\.$#', 'count' => 1, @@ -8607,7 +8592,7 @@ 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; $ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between mixed and \'\\(\\ '#^Binary operation "\\." between string and mixed results in an error\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', ]; diff --git a/.phpstan/baseline/cast.string.php b/.phpstan/baseline/cast.string.php index b5d0a3f57b35..df6af0763471 100644 --- a/.phpstan/baseline/cast.string.php +++ b/.phpstan/baseline/cast.string.php @@ -1803,14 +1803,9 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 13, + 'count' => 6, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot cast mixed to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot cast mixed to string\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/echo.nonString.php b/.phpstan/baseline/echo.nonString.php index 43c7e46209da..b299b8f3f888 100644 --- a/.phpstan/baseline/echo.nonString.php +++ b/.phpstan/baseline/echo.nonString.php @@ -516,31 +516,16 @@ 'count' => 4, 'path' => __DIR__ . '/../../interface/new/new_comprehensive.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', 'count' => 3, 'path' => __DIR__ . '/../../interface/orders/orders_results.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_list.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/single_order_results.inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/types.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', 'count' => 1, @@ -741,11 +726,6 @@ 'count' => 4, 'path' => __DIR__ . '/../../interface/super/edit_layout.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout_props.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', 'count' => 30, @@ -758,12 +738,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', - 'count' => 12, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#', - 'count' => 5, + 'count' => 4, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/encapsedStringPart.nonString.php b/.phpstan/baseline/encapsedStringPart.nonString.php index cb828c428e88..08f83270a2e0 100644 --- a/.phpstan/baseline/encapsedStringPart.nonString.php +++ b/.phpstan/baseline/encapsedStringPart.nonString.php @@ -216,26 +216,6 @@ 'count' => 3, 'path' => __DIR__ . '/../../index.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 4, @@ -2161,11 +2141,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/new/new_search_popup.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$sub \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$protocol \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 2, @@ -2176,21 +2151,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../interface/orders/gen_hl7_order.inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$webserver_root \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/gen_hl7_order.inc.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$include_root \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/list_reports.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/list_reports.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$diagnoses \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 1, @@ -2226,46 +2186,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/order_manifest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/order_manifest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/orders/orders_results.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$form_key \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/patient_match_dialog.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/patient_match_dialog.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/pending_orders.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_edit.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_stats.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$commentdelim \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 6, @@ -2341,11 +2266,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/single_order_results.inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/types_edit.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$formdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 2, @@ -3206,16 +3126,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/reports/unique_seen_patients_report.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$webserver_root \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$colstr \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 2, @@ -3251,11 +3161,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_layout.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$tablename \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 3, @@ -3266,11 +3171,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../interface/super/edit_layout_props.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/edit_list.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$code \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 1, @@ -3286,11 +3186,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$OE_SITE_DIR \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/manage_document_templates.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$form_dest_filename \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 1, @@ -3301,81 +3196,16 @@ 'count' => 3, 'path' => __DIR__ . '/../../interface/super/manage_document_templates.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$OE_SITE_DIR \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/manage_site_files.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$fld \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/addrbook_edit.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/addrbook_edit.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/addrbook_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facilities_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facility_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facility_user.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facility_user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/mfa_registrations.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/mfa_totp.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/mfa_u2f.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_info.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$facid \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 2, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Part \\$srcdir \\(mixed\\) of encapsed string cannot be cast to string\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; $ignoreErrors[] = [ 'message' => '#^Part \\$dbase \\(mixed\\) of encapsed string cannot be cast to string\\.$#', 'count' => 2, diff --git a/.phpstan/baseline/foreach.nonIterable.php b/.phpstan/baseline/foreach.nonIterable.php index b7d7045ae022..0d60c154b80d 100644 --- a/.phpstan/baseline/foreach.nonIterable.php +++ b/.phpstan/baseline/foreach.nonIterable.php @@ -1393,12 +1393,12 @@ ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 8, + 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_globals.php', ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 9, + 'count' => 7, 'path' => __DIR__ . '/../../interface/super/edit_layout.php', ]; $ignoreErrors[] = [ @@ -1406,11 +1406,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_list.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', 'count' => 4, @@ -1448,32 +1443,32 @@ ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 5, + 'count' => 4, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 5, + 'count' => 4, 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 8, + 'count' => 3, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'count' => 7, + 'count' => 2, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/function.alreadyNarrowedType.php b/.phpstan/baseline/function.alreadyNarrowedType.php index dbdb0544646d..921fa2d6b658 100644 --- a/.phpstan/baseline/function.alreadyNarrowedType.php +++ b/.phpstan/baseline/function.alreadyNarrowedType.php @@ -81,6 +81,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/patient_file/encounter/forms.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Call to function is_string\\(\\) with string will always evaluate to true\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../interface/super/edit_globals.php', +]; $ignoreErrors[] = [ 'message' => '#^Call to function is_array\\(\\) with array\\{array\\{callback_url\\: non\\-falsy\\-string, providers\\?\\: non\\-empty\\-list\\, facilities\\?\\: non\\-empty\\-list\\, categories\\?\\: non\\-empty\\-list\\, apptstats\\?\\: non\\-empty\\-list\\, checkedOut\\?\\: non\\-empty\\-list\\, clinical_reminders\\?\\: non\\-empty\\-list\\\\}\\} will always evaluate to true\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/function.impossibleType.php b/.phpstan/baseline/function.impossibleType.php index 0f7b1909fa81..7ca499da6f48 100644 --- a/.phpstan/baseline/function.impossibleType.php +++ b/.phpstan/baseline/function.impossibleType.php @@ -21,6 +21,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/patient_file/summary/pnotes_full.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Call to function is_null\\(\\) with array\\ will always evaluate to false\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', +]; $ignoreErrors[] = [ 'message' => '#^Call to function is_countable\\(\\) with string will always evaluate to false\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/isset.variable.php b/.phpstan/baseline/isset.variable.php index 0478128c91a4..74ab76a5088d 100644 --- a/.phpstan/baseline/isset.variable.php +++ b/.phpstan/baseline/isset.variable.php @@ -116,6 +116,16 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_list.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$addStatus in isset\\(\\) always exists and is not nullable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$message in isset\\(\\) always exists and is not nullable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', +]; $ignoreErrors[] = [ 'message' => '#^Variable \\$res in isset\\(\\) always exists and is not nullable\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/nullCoalesce.variable.php b/.phpstan/baseline/nullCoalesce.variable.php index fa63d5f31625..14dd1b303f0d 100644 --- a/.phpstan/baseline/nullCoalesce.variable.php +++ b/.phpstan/baseline/nullCoalesce.variable.php @@ -491,6 +491,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/procedure_provider_edit.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$arep on left side of \\?\\? is never defined\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../interface/orders/receive_hl7_results.inc.php', +]; $ignoreErrors[] = [ 'message' => '#^Variable \\$attendant_type on left side of \\?\\? is never defined\\.$#', 'count' => 1, @@ -546,6 +551,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/facility_admin.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$uuid on left side of \\?\\? always exists and is not nullable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', +]; $ignoreErrors[] = [ 'message' => '#^Variable \\$events on left side of \\?\\? is never defined\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/offsetAccess.invalidOffset.php b/.phpstan/baseline/offsetAccess.invalidOffset.php index 9b6034c925d0..255c728c1ac7 100644 --- a/.phpstan/baseline/offsetAccess.invalidOffset.php +++ b/.phpstan/baseline/offsetAccess.invalidOffset.php @@ -576,11 +576,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/reports/report_results.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Possibly invalid array key type mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; $ignoreErrors[] = [ 'message' => '#^Possibly invalid array key type mixed\\.$#', 'count' => 2, @@ -601,6 +596,21 @@ 'count' => 6, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_controllers/therapy_groups_controller.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Possibly invalid array key type mixed\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Possibly invalid array key type mixed\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Possibly invalid array key type mixed\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', +]; $ignoreErrors[] = [ 'message' => '#^Possibly invalid array key type mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/offsetAccess.nonArray.php b/.phpstan/baseline/offsetAccess.nonArray.php index 824954d49b68..ca0c6330b51d 100644 --- a/.phpstan/baseline/offsetAccess.nonArray.php +++ b/.phpstan/baseline/offsetAccess.nonArray.php @@ -106,11 +106,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/receive_hl7_results.inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot use array destructuring on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot use array destructuring on mixed\\.$#', 'count' => 6, diff --git a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php index b7bf3619b00b..d22ee2e7b288 100644 --- a/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php +++ b/.phpstan/baseline/offsetAccess.nonOffsetAccessible.php @@ -1846,11 +1846,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../interface/batchcom/batch_reminders.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'type\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batch_reminders.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset 0 on mixed\\.$#', 'count' => 1, @@ -1861,11 +1856,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/batchcom/batch_reminders.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset 1 on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'authorized\' on mixed\\.$#', 'count' => 2, @@ -20663,12 +20653,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'procedure_code\' on array\\|false\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'procedure_code\' on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', ]; $ignoreErrors[] = [ @@ -20678,12 +20663,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'procedure_type_name\' on array\\|false\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'procedure_type_name\' on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', ]; $ignoreErrors[] = [ @@ -20692,7 +20672,7 @@ 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'transport\' on mixed\\.$#', + 'message' => '#^Cannot access offset \'transport\' on array\\|false\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', ]; @@ -21056,11 +21036,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/orders/pending_followup.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'id\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_edit.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'lab_director\' on array\\|false\\.$#', 'count' => 1, @@ -27121,11 +27096,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_globals.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset 4 on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'action\' on mixed\\.$#', 'count' => 2, @@ -27626,26 +27596,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/super/edit_list.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'active\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'code_text\' on array\\|false\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'id\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'nofs\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'size\' on mixed\\.$#', 'count' => 1, @@ -27663,11 +27618,6 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset non\\-falsy\\-string on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset string on mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', ]; @@ -27891,56 +27841,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_models/users_model.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'counselors\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'fname\' on mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_guest_counselors\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_id\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_name\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_notes\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_participation\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_start_date\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_status\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_type\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'id\' on mixed\\.$#', 'count' => 2, @@ -27951,11 +27856,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_id\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/appointmentComponent.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'pc_apptstatus\' on mixed\\.$#', 'count' => 1, @@ -27996,61 +27896,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/appointmentComponent.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'counselors\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'fname\' on mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_end_date\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_guest_counselors\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_id\' on mixed\\.$#', - 'count' => 8, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_name\' on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_notes\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_participation\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_start_date\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_status\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_type\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'id\' on mixed\\.$#', 'count' => 2, @@ -28061,46 +27911,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'fname\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_patient_comment\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_patient_end\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_patient_start\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'group_patient_status\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'lname\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'participant_name\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'pid\' on mixed\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'counselors\' on mixed\\.$#', 'count' => 1, @@ -28151,14 +27961,9 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset 10 on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset mixed on mixed\\.$#', - 'count' => 2, + 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', ]; $ignoreErrors[] = [ @@ -28486,186 +28291,26 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/mfa_u2f.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'active\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'authorized\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'billing_facility_id\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'calendar\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'default_warehouse\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'email\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'facility_id\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'federaldrugid\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'federaltaxid\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'fname\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'google_signin_email\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'id\' on mixed\\.$#', 'count' => 9, 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'info\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'irnpool\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'lname\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'main_menu_role\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'mname\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'name\' on mixed\\.$#', 'count' => 3, 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'newcrop_user_role\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'npi\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'patient_menu_role\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'physician_type\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'portal_user\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'rules\' on mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'see_auth\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'specialty\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'state_license_number\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'suffix\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'supervisor_id\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'taxonomy\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'upin\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'user_form\' on mixed\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'username\' on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'valedictory\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'weno_prov_id\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset 0 on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset int\\<0, max\\> on array\\|Countable\\.$#', 'count' => 2, @@ -28673,7 +28318,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset int\\<0, max\\> on mixed\\.$#', - 'count' => 4, + 'count' => 2, 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', ]; $ignoreErrors[] = [ @@ -28701,56 +28346,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/user_info.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'active\' on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'authorized\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'email\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'fname\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'id\' on mixed\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'info\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'last_update_password\' on array\\|false\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'lname\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'name\' on mixed\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'user\' on mixed\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'username\' on array\\{id\\: numeric\\-string, uuid\\: string\\|null, username\\: string\\|null, password\\: string\\|null, authorized\\: numeric\\-string\\|null, info\\: string\\|null, source\\: numeric\\-string\\|null, fname\\: string\\|null, \\.\\.\\.\\}\\|false\\.$#', 'count' => 1, @@ -28761,11 +28361,6 @@ 'count' => 3, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'username\' on mixed\\.$#', - 'count' => 6, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset int\\<0, max\\> on array\\|Countable\\.$#', 'count' => 1, @@ -28773,16 +28368,11 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset int\\<0, max\\> on mixed\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset int\\<0, max\\> on non\\-empty\\-array\\|Countable\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot access offset mixed on mixed\\.$#', + 'message' => '#^Cannot access offset int\\<0, max\\> on non\\-empty\\-array\\|Countable\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', ]; @@ -28793,12 +28383,12 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'id\' on mixed\\.$#', - 'count' => 4, + 'count' => 3, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'name\' on mixed\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', ]; $ignoreErrors[] = [ @@ -28816,16 +28406,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'user\' on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'username\' on mixed\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'weno_prov_id\' on mixed\\.$#', 'count' => 1, @@ -28833,12 +28413,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset int\\<0, max\\> on mixed\\.$#', - 'count' => 6, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset mixed on mixed\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', ]; $ignoreErrors[] = [ diff --git a/.phpstan/baseline/postInc.type.php b/.phpstan/baseline/postInc.type.php index 4b103ba7c602..c904499aa169 100644 --- a/.phpstan/baseline/postInc.type.php +++ b/.phpstan/baseline/postInc.type.php @@ -6,11 +6,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../gacl/profiler.inc.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot use \\+\\+ on mixed\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchEmail.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot use \\+\\+ on mixed\\.$#', 'count' => 1, diff --git a/.phpstan/baseline/variable.undefined.php b/.phpstan/baseline/variable.undefined.php index 0db28961a13a..b0a8d65ead62 100644 --- a/.phpstan/baseline/variable.undefined.php +++ b/.phpstan/baseline/variable.undefined.php @@ -361,171 +361,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../index.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$m_error might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchEmail.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$m_error_count might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/batchcom/batchEmail.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$res might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchEmail.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$res might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchPhoneList.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$results_log might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batch_reminders.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$send_rem_log might not be defined\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/batchcom/batch_reminders.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$file might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.inc.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$flag_on might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.inc.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$line might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.inc.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$choices might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$form_err might not be defined\\.$#', - 'count' => 11, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/batchcom.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$email_sender might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$email_subject might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$form_err might not be defined\\.$#', - 'count' => 6, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$message might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$notification_id might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$provider_name might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/emailnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$SMS_gateway_apikey might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$SMS_gateway_password might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$SMS_gateway_username might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$Send_Email_Before_Hours might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$Send_SMS_Before_Hours might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$SettingsId might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$form_err might not be defined\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/settingsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$form_err might not be defined\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$message might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$notification_id might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$provider_name might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$sms_gateway_type might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/batchcom/smsnotification.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$CheckBoxBilling might not be defined\\.$#', 'count' => 2, @@ -10516,146 +10351,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/new/new_search_popup.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$codes might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$proctype_name might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$ptrow might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$sub might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$testid might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$transport might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$webroot might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/find_order_popup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$webserver_root might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/gen_hl7_order.inc.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$include_root might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/list_reports.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$pid might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/list_reports.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/list_reports.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$ptid might not be defined\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/orders/load_compendium.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/order_manifest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$pid might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/orders/orders_results.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/orders/orders_results.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/patient_match_dialog.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/pending_orders.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$org_row might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_edit.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_edit.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$help_icon might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_provider_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/procedure_stats.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Undefined variable\\: \\$arep$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/orders/receive_hl7_results.inc.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$help_icon might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/types.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$webroot might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/types.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$level might not be defined\\.$#', - 'count' => 6, - 'path' => __DIR__ . '/../../interface/orders/types_ajax.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/types_edit.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$title might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/orders/types_edit.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$pid might not be defined\\.$#', 'count' => 1, @@ -12596,151 +12291,6 @@ 'count' => 2, 'path' => __DIR__ . '/../../interface/smart/register-app.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$GLOBALS_METADATA might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$USER_SPECIFIC_GLOBALS might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$USER_SPECIFIC_TABS might not be defined\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$globalTitle might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$globalValue might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$webserver_root might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_globals.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$datatypes might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$sources might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$typesUsingList might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$rootdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_layout_props.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$code might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/super/edit_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$ok_map_cvx_codes might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/edit_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$total_rows might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/edit_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$code_types might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$eres might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$tmp might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/layout_service_codes.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$OE_SITE_DIR might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/manage_document_templates.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$OE_SITE_DIR might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/manage_site_files.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$fldname might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/templates/field_html_display_section.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$userMode might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/super/templates/field_multi_sorted_list_selector.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$therapy_group might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/index.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$encounters might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_models/therapy_groups_encounters_model.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$providers might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_models/users_model.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$groupData might not be defined\\.$#', - 'count' => 15, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$message might not be defined\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$savingStatus might not be defined\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/addGroup.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$statuses might not be defined\\.$#', 'count' => 1, @@ -12756,43 +12306,8 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/appointmentComponent.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$groupData might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/appointmentComponent.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$groupId might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/appointmentComponent.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$groupData might not be defined\\.$#', - 'count' => 26, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$groupId might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$message might not be defined\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$readonly might not be defined\\.$#', - 'count' => 13, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$savingStatus might not be defined\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$statuses might not be defined\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; @@ -12801,36 +12316,11 @@ 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsGeneralData.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$addStatus might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$groupId might not be defined\\.$#', - 'count' => 9, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$groupName might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$group_id might not be defined\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$participant_data might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$participants might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/groupDetailsParticipants.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$readonly might not be defined\\.$#', 'count' => 2, @@ -12861,186 +12351,11 @@ 'count' => 2, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$statuses might not be defined\\.$#', - 'count' => 5, - 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$therapyGroups might not be defined\\.$#', 'count' => 1, 'path' => __DIR__ . '/../../interface/therapy_groups/therapy_groups_views/listGroups.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$row might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/addrbook_edit.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/addrbook_edit.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/addrbook_list.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result2 might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facilities.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facilities_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facility_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facility_user.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$date_init might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facility_user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/facility_user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/mfa_registrations.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/mfa_totp.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/mfa_u2f.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$email might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/ssl_certificates_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$user might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/ssl_certificates_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$selected_user_is_superuser might not be defined\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/user_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_info.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$webroot might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/user_info.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$SMTP_HOST might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$current_date might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$grace_time might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$grouplist might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$pwd_expires might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result4 might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result5 might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$success might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$un might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$grouplist might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result3 might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$result5 might not be defined\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$srcdir might not be defined\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/../../interface/usergroup/usergroup_admin_add.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$dbase might not be defined\\.$#', 'count' => 2, diff --git a/.phpstan/fatal-baseline-caps.php b/.phpstan/fatal-baseline-caps.php index 540c596a8ae6..808f1d4863cb 100644 --- a/.phpstan/fatal-baseline-caps.php +++ b/.phpstan/fatal-baseline-caps.php @@ -48,7 +48,7 @@ 'return.missing.php' => 0, 'staticMethod.notFound.php' => 0, 'trait.notFound.php' => 0, - 'variable.undefined.php' => 3064, + 'variable.undefined.php' => 2927, ], 'confidentNonObject' => [ 'classConstant.nonObject.php' => 0, diff --git a/interface/batchcom/batchEmail.php b/interface/batchcom/batchEmail.php index 0c699a10d891..6472fd6d3666 100644 --- a/interface/batchcom/batchEmail.php +++ b/interface/batchcom/batchEmail.php @@ -20,10 +20,20 @@ use OpenEMR\Common\Session\SessionWrapperFactory; use OpenEMR\Core\Header; +// This script is meant to be required from batchcom.php, which sets $res +// (the SQL result of the patient query). Direct access has nothing to send. +if (!isset($res)) { + require_once(__DIR__ . '/batchcom.php'); + exit; +} + $session = SessionWrapperFactory::getInstance()->getActiveSession(); CsrfUtils::checkCsrfInput(INPUT_POST, dieOnFail: true); +$m_error = false; +$m_error_count = 0; + ?> @@ -71,7 +81,7 @@ ' . xlt('Could not send email due to a server problem.') . ' ' . text($m_error_count) . ' ' . xlt('emails not sent') . ''; + echo '
' . xlt('Could not send email due to a server problem.') . ' ' . text((string) $m_error_count) . ' ' . xlt('emails not sent') . '
'; } ?> diff --git a/interface/batchcom/batchPhoneList.php b/interface/batchcom/batchPhoneList.php index dd4e97b69687..efde700758ff 100644 --- a/interface/batchcom/batchPhoneList.php +++ b/interface/batchcom/batchPhoneList.php @@ -19,6 +19,13 @@ use OpenEMR\Common\Session\SessionWrapperFactory; use OpenEMR\Core\Header; +// This script is meant to be required from batchcom.php, which sets $res +// (the SQL result of the patient query). Direct access has nothing to render. +if (!isset($res)) { + require_once(__DIR__ . '/batchcom.php'); + exit; +} + $session = SessionWrapperFactory::getInstance()->getActiveSession(); CsrfUtils::checkCsrfInput(INPUT_POST, dieOnFail: true); diff --git a/interface/batchcom/batch_reminders.php b/interface/batchcom/batch_reminders.php index e43465c6e755..8fa603c92641 100644 --- a/interface/batchcom/batch_reminders.php +++ b/interface/batchcom/batch_reminders.php @@ -64,6 +64,8 @@
'', 'date_report' => '', 'data' => '']; + $send_rem_log = []; if ($report_id) { // collect log from a previous run to show $results_log = collectReportDatabase($report_id); diff --git a/interface/batchcom/batchcom.inc.php b/interface/batchcom/batchcom.inc.php index e2c107c0a8d3..eca880a37b79 100644 --- a/interface/batchcom/batchcom.inc.php +++ b/interface/batchcom/batchcom.inc.php @@ -67,6 +67,8 @@ function generate_csv($sql_result): void // create file header. // menu for fields could be added in the future + $flag_on = false; + $file = ''; while ($row = sqlFetchArray($sql_result)) { if (!$flag_on) { $flag_on = true; @@ -79,6 +81,7 @@ function generate_csv($sql_result): void reset($row); } + $line = ''; foreach ($row as $value) { $line .= csvEscape($value) . ","; } @@ -86,7 +89,6 @@ function generate_csv($sql_result): void $line = substr($line, 0, -1); $line .= "\n"; $file .= $line; - $line = ''; } //download diff --git a/interface/batchcom/batchcom.php b/interface/batchcom/batchcom.php index d471ca06c34b..919eb8d2fe77 100644 --- a/interface/batchcom/batchcom.php +++ b/interface/batchcom/batchcom.php @@ -14,7 +14,7 @@ //INCLUDES, DO ANY ACTIONS, THEN GET OUR DATA require_once("../globals.php"); -require_once("$srcdir/registry.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/registry.inc.php"); require_once("batchcom.inc.php"); use OpenEMR\Common\Acl\AccessDeniedHelper; @@ -24,6 +24,8 @@ use OpenEMR\Core\Header; use OpenEMR\Core\OEGlobalsBag; +$form_err = ''; + if (!AclMain::aclCheckCore('admin', 'batchcom')) { AccessDeniedHelper::denyWithTemplate("ACL check failed for admin/batchcom: BatchCom", xl("BatchCom")); } @@ -140,7 +142,7 @@ } switch ($_POST['process_type']) : - case $choices[1]: // Email + case $process_choices[1]: // Email $sql .= " and patient_data.email IS NOT NULL "; break; endswitch; diff --git a/interface/batchcom/emailnotification.php b/interface/batchcom/emailnotification.php index 55357e1b121a..4aafe2f86125 100755 --- a/interface/batchcom/emailnotification.php +++ b/interface/batchcom/emailnotification.php @@ -13,7 +13,7 @@ */ require_once("../globals.php"); -require_once("$srcdir/registry.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/registry.inc.php"); require_once("batchcom.inc.php"); use OpenEMR\Common\Acl\AccessDeniedHelper; @@ -27,6 +27,13 @@ AccessDeniedHelper::denyWithTemplate("ACL check failed for admin/notification: Email Notification", xl("Email Notification")); } +$form_err = ''; +$notification_id = ''; +$provider_name = ''; +$email_sender = ''; +$email_subject = ''; +$message = ''; + // process form $session = SessionWrapperFactory::getInstance()->getActiveSession(); if (!empty($_POST['form_action']) && ($_POST['form_action'] == 'save')) { diff --git a/interface/batchcom/settingsnotification.php b/interface/batchcom/settingsnotification.php index 46c7c397e2cc..94972af51a64 100755 --- a/interface/batchcom/settingsnotification.php +++ b/interface/batchcom/settingsnotification.php @@ -13,7 +13,7 @@ */ require_once("../globals.php"); -require_once("$srcdir/registry.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/registry.inc.php"); require_once("batchcom.inc.php"); use OpenEMR\Common\Acl\AccessDeniedHelper; @@ -27,6 +27,14 @@ AccessDeniedHelper::denyWithTemplate("ACL check failed for admin/notification: Notification Settings", xl("Notification Settings")); } +$form_err = ''; +$SettingsId = ''; +$Send_SMS_Before_Hours = ''; +$Send_Email_Before_Hours = ''; +$SMS_gateway_password = ''; +$SMS_gateway_username = ''; +$SMS_gateway_apikey = ''; + $type = 'SMS/Email Settings'; // process form $session = SessionWrapperFactory::getInstance()->getActiveSession(); diff --git a/interface/batchcom/smsnotification.php b/interface/batchcom/smsnotification.php index d0088e65314a..ea994212bd93 100755 --- a/interface/batchcom/smsnotification.php +++ b/interface/batchcom/smsnotification.php @@ -13,7 +13,7 @@ */ require_once("../globals.php"); -require_once("$srcdir/registry.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/registry.inc.php"); require_once("batchcom.inc.php"); use OpenEMR\Common\Acl\AccessDeniedHelper; @@ -27,6 +27,12 @@ AccessDeniedHelper::denyWithTemplate("ACL check failed for admin/notification: SMS Notification", xl("SMS Notification")); } +$form_err = ''; +$notification_id = ''; +$sms_gateway_type = ''; +$provider_name = ''; +$message = ''; + // process form $session = SessionWrapperFactory::getInstance()->getActiveSession(); if (!empty($_POST['form_action']) && ($_POST['form_action'] == 'save')) { diff --git a/interface/orders/find_order_popup.php b/interface/orders/find_order_popup.php index 37a4a5478a12..267c3fbcc498 100644 --- a/interface/orders/find_order_popup.php +++ b/interface/orders/find_order_popup.php @@ -26,6 +26,11 @@ $grporders = []; $typeid = (int) $_GET['typeid']; $name = ''; + $ptrow = []; + $codes = ''; + $transport = ''; + $testid = ''; + $proctype_name = ''; if ($typeid) { $ptrow = sqlQuery("SELECT * FROM procedure_type WHERE procedure_type_id = ?", [$typeid]); $name = $ptrow['name']; @@ -43,7 +48,7 @@ } } ?> - + + diff --git a/interface/usergroup/mfa_registrations.php b/interface/usergroup/mfa_registrations.php index eec1a90a600f..0ae4ddf1725e 100644 --- a/interface/usergroup/mfa_registrations.php +++ b/interface/usergroup/mfa_registrations.php @@ -13,7 +13,7 @@ */ require_once("../globals.php"); -require_once("$srcdir/options.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"); use OpenEMR\Common\Csrf\CsrfUtils; use OpenEMR\Common\Session\SessionWrapperFactory; diff --git a/interface/usergroup/mfa_totp.php b/interface/usergroup/mfa_totp.php index 56cb6307bf62..c5b812770518 100644 --- a/interface/usergroup/mfa_totp.php +++ b/interface/usergroup/mfa_totp.php @@ -17,8 +17,8 @@ // Set $sessionAllowWrite to true to prevent session concurrency issues during authorization related code $sessionAllowWrite = true; require_once('../globals.php'); -require_once("$srcdir/classes/Totp.class.php"); -require_once("$srcdir/options.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/classes/Totp.class.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"); use OpenEMR\BC\ServiceContainer; use OpenEMR\Common\Auth\AuthUtils; diff --git a/interface/usergroup/mfa_u2f.php b/interface/usergroup/mfa_u2f.php index bdc8df29f4d3..d1ee04397a27 100644 --- a/interface/usergroup/mfa_u2f.php +++ b/interface/usergroup/mfa_u2f.php @@ -13,7 +13,7 @@ */ require_once('../globals.php'); -require_once("$srcdir/options.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"); use OpenEMR\Common\Csrf\CsrfUtils; use OpenEMR\Common\Session\SessionWrapperFactory; diff --git a/interface/usergroup/ssl_certificates_admin.php b/interface/usergroup/ssl_certificates_admin.php index 7f5dccee3e58..5de5e36989a1 100644 --- a/interface/usergroup/ssl_certificates_admin.php +++ b/interface/usergroup/ssl_certificates_admin.php @@ -87,10 +87,12 @@ function create_client_cert(): void return; } + $user = ''; if ($_POST["client_cert_user"]) { $user = trim((string) $_POST['client_cert_user']); } + $email = ''; if ($_POST["client_cert_email"]) { $email = trim((string) $_POST['client_cert_email']); } diff --git a/interface/usergroup/user_admin.php b/interface/usergroup/user_admin.php index 680eab14eefe..273564387307 100644 --- a/interface/usergroup/user_admin.php +++ b/interface/usergroup/user_admin.php @@ -17,8 +17,8 @@ */ require_once("../globals.php"); -require_once("$srcdir/calendar.inc.php"); -require_once("$srcdir/options.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/calendar.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"); use OpenEMR\Common\Acl\AccessDeniedHelper; use OpenEMR\Common\Acl\AclExtended; @@ -49,6 +49,7 @@ } $res = sqlStatement("select * from users where id=?", [$_GET["id"]]); +$result = []; for ($iter = 0; $row = sqlFetchArray($res); $iter++) { $result[$iter] = $row; } @@ -257,9 +258,9 @@ function toggle_password() { $is_super_user = AclMain::aclCheckCore('admin', 'super'); $acl_name = AclExtended::aclGetGroupTitles($iter["username"]); $bg_name = ''; + $selected_user_is_superuser = false; if (is_countable($acl_name)) { $bg_count = count($acl_name); - $selected_user_is_superuser = false; for ($i = 0; $i < $bg_count; $i++) { if ($acl_name[$i] == "Emergency Login") { $bg_name = $acl_name[$i]; @@ -369,6 +370,7 @@ function toggle_password() { getAllServiceLocations(); if ($fres) { + $result = []; for ($iter2 = 0; $iter2 < count($fres); $iter2++) { $result[$iter2] = $fres[$iter2]; } diff --git a/interface/usergroup/user_info.php b/interface/usergroup/user_info.php index 1b892904a175..3a0a7611361e 100644 --- a/interface/usergroup/user_info.php +++ b/interface/usergroup/user_info.php @@ -16,7 +16,7 @@ */ require_once("../globals.php"); -require_once("$srcdir/user.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/user.inc.php"); use OpenEMR\Common\Auth\AuthUtils; use OpenEMR\Common\Csrf\CsrfUtils; @@ -43,7 +43,7 @@ - + getSrcDir() . '/../interface/reports/report.script.php'; ?> getSrcDir() . "/ajax/payment_ajax_jav.inc.php"; ?> @@ -728,7 +732,7 @@ function criteriaSelectHasValue(select) { } ?> getSrcDir() . '/../interface/reports/criteria.tab.php'; ?> @@ -923,6 +927,7 @@ function criteriaSelectHasValue(select) { $mmo_num_charges = 0; $encount = 0; $divPut = false; + $CheckBoxBilling = 0; foreach ($ret as $iter) { // We include encounters here that have never been billed. However @@ -1018,6 +1023,8 @@ function criteriaSelectHasValue(select) { $lcount = 1; $rcount = 0; $oldcode = ""; + $enc_billing_note ??= []; + $default_x12_partner = null; $ptname = $name['fname'] . " " . $name['lname']; $raw_encounter_date = date("Y-m-d", strtotime((string) $iter['enc_date'])); @@ -1318,8 +1325,8 @@ function criteriaSelectHasValue(select) { if ($tmpbpr == '0' && $iter['billed']) { $tmpbpr = '2'; } - $rhtml .= " \n"; - $CheckBoxBilling = ($CheckBoxBilling ?? null) + 1; + $rhtml .= " \n"; + $CheckBoxBilling++; } else { $rhtml .= "\n"; } @@ -1367,7 +1374,7 @@ function criteriaSelectHasValue(select) { $rhtml2 .= "\n"; } if (!$iter['id'] && $rowcnt == 1) { - $rhtml2 .= " \n"; + $rhtml2 .= " \n"; $CheckBoxBilling++; } else { $rhtml2 .= "\n"; diff --git a/interface/billing/billing_tracker.php b/interface/billing/billing_tracker.php index 4f1a2450c6fb..7a1681cdc4e6 100644 --- a/interface/billing/billing_tracker.php +++ b/interface/billing/billing_tracker.php @@ -14,7 +14,6 @@ */ require_once(__DIR__ . "/../globals.php"); -require_once "$srcdir/options.inc.php"; use OpenEMR\Common\{ Acl\AccessDeniedHelper, @@ -26,6 +25,8 @@ use OpenEMR\Core\Header; use OpenEMR\Core\OEGlobalsBag; +require_once OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"; + //ensure user has proper access if (!AclMain::aclCheckCore('acct', 'eob', '', 'write') && !AclMain::aclCheckCore('acct', 'bill', '', 'write')) { AccessDeniedHelper::denyWithTemplate("ACL check failed for acct/eob or acct/bill: Billing Manager", xl("Billing Manager")); diff --git a/interface/billing/edi_270.php b/interface/billing/edi_270.php index 8c38ea6d3763..af8be7d2329a 100644 --- a/interface/billing/edi_270.php +++ b/interface/billing/edi_270.php @@ -21,11 +21,6 @@ */ require_once("../globals.php"); -require_once("$srcdir/forms.inc.php"); -require_once("$srcdir/patient.inc.php"); -require_once "$srcdir/options.inc.php"; -require_once("$srcdir/calendar.inc.php"); -require_once("$srcdir/appointments.inc.php"); use OpenEMR\Billing\EDI270; use OpenEMR\Common\Csrf\CsrfUtils; @@ -33,6 +28,13 @@ use OpenEMR\Core\Header; use OpenEMR\Core\OEGlobalsBag; +$srcDir = OEGlobalsBag::getInstance()->getSrcDir(); +require_once($srcDir . '/forms.inc.php'); +require_once($srcDir . '/patient.inc.php'); +require_once($srcDir . '/options.inc.php'); +require_once($srcDir . '/calendar.inc.php'); +require_once($srcDir . '/appointments.inc.php'); + $session = SessionWrapperFactory::getInstance()->getActiveSession(); if (!empty($_POST)) { diff --git a/interface/billing/edi_271.php b/interface/billing/edi_271.php index 13c55080b894..d882316e81b1 100644 --- a/interface/billing/edi_271.php +++ b/interface/billing/edi_271.php @@ -15,10 +15,6 @@ */ require_once(__DIR__ . "/../globals.php"); -require_once("$srcdir/forms.inc.php"); -require_once("$srcdir/patient.inc.php"); -require_once("$srcdir/report.inc.php"); -require_once("$srcdir/calendar.inc.php"); use OpenEMR\BC\ServiceContainer; use OpenEMR\Billing\EDI270; @@ -28,6 +24,12 @@ use OpenEMR\Core\Header; use OpenEMR\Core\OEGlobalsBag; +$srcDir = OEGlobalsBag::getInstance()->getSrcDir(); +require_once($srcDir . '/forms.inc.php'); +require_once($srcDir . '/patient.inc.php'); +require_once($srcDir . '/report.inc.php'); +require_once($srcDir . '/calendar.inc.php'); + $session = SessionWrapperFactory::getInstance()->getActiveSession(); if (!empty($_POST)) { CsrfUtils::checkCsrfInput(INPUT_POST, dieOnFail: true); @@ -36,6 +38,7 @@ // File location (URL or server path) $target = OEGlobalsBag::getInstance()->get('edi_271_file_path'); $batch_log = ''; +$message = ''; if (isset($_FILES) && !empty($_FILES)) { $uploadedName = (string) $_FILES['uploaded']['name']; @@ -51,7 +54,7 @@ if (in_array($uploadedExt, ['inc', 'php', 'php7', 'php8'], true)) { $message .= xlt('Invalid file type.') . "
"; } - if (!isset($message)) { + if ($message === '') { $cryptoGen = ServiceContainer::getCrypto(); $uploadedFile = file_get_contents($_FILES['uploaded']['tmp_name']); if (OEGlobalsBag::getInstance()->getBoolean('drive_encryption')) { @@ -134,7 +137,7 @@ function edivalidation() { - +
getSrcDir(); +require_once($srcDir . '/edihistory/edih_csv_inc.php'); +require_once($srcDir . '/edihistory/edih_io.php'); +require_once($srcDir . '/edihistory/edih_x12file_class.php'); +require_once($srcDir . '/edihistory/edih_uploads.php'); +require_once($srcDir . '/edihistory/edih_csv_parse.php'); +require_once($srcDir . '/edihistory/edih_csv_data.php'); +require_once($srcDir . '/edihistory/edih_997_error.php'); +require_once($srcDir . '/edihistory/edih_segments.php'); +require_once($srcDir . '/edihistory/edih_archive.php'); +require_once($srcDir . '/edihistory/edih_271_html.php'); +require_once($srcDir . '/edihistory/edih_277_html.php'); +require_once($srcDir . '/edihistory/edih_278_html.php'); +require_once($srcDir . '/edihistory/edih_835_html.php'); +require_once($srcDir . '/edihistory/codes/edih_271_code_class.php'); +require_once($srcDir . '/edihistory/codes/edih_835_code_class.php'); +require_once($srcDir . '/edihistory/codes/edih_997_codes.php'); // // php may output line endings with included files ob_clean(); @@ -96,6 +97,7 @@ // if we are not set up, create directories and csv files //if (!is_dir(__DIR__ . '/edihist' . IBR_HISTORY_DIR) ) { +$html_str = ''; if (!is_dir($edih_tmp_dir)) { // //echo "setup with base directory: $edih_base_dir
" .PHP_EOL; diff --git a/interface/billing/edih_view.php b/interface/billing/edih_view.php index eccb862d8a20..75b9c0adf3cb 100644 --- a/interface/billing/edih_view.php +++ b/interface/billing/edih_view.php @@ -262,7 +262,7 @@
- +
diff --git a/interface/billing/edit_payment.php b/interface/billing/edit_payment.php index 0e7bfb378454..1f2f4ca0cc2a 100644 --- a/interface/billing/edit_payment.php +++ b/interface/billing/edit_payment.php @@ -22,10 +22,6 @@ require_once("../globals.php"); require_once("../../custom/code_types.inc.php"); -require_once("$srcdir/patient.inc.php"); -require_once("$srcdir/options.inc.php"); -require_once("$srcdir/payment.inc.php"); - use OpenEMR\Common\Acl\AccessDeniedHelper; use OpenEMR\Common\Acl\AclMain; use OpenEMR\Common\Session\SessionWrapperFactory; @@ -33,7 +29,18 @@ use OpenEMR\Core\OEGlobalsBag; use OpenEMR\PaymentProcessing\Recorder; +$srcDir = OEGlobalsBag::getInstance()->getSrcDir(); +require_once($srcDir . '/patient.inc.php'); +require_once($srcDir . '/options.inc.php'); +require_once($srcDir . '/payment.inc.php'); + $session = SessionWrapperFactory::getInstance()->getActiveSession(); +$CountIndexAbove = 0; +$CountIndexBelow = 0; +$TypeCode = ''; +$AccountCode = ''; +$AdjustString = ''; +$bgcolor = ''; if (!AclMain::aclCheckCore('acct', 'bill', '', 'write') && !AclMain::aclCheckCore('acct', 'eob', '', 'write')) { AccessDeniedHelper::denyWithTemplate("ACL check failed for acct/bill or acct/eob: Confirm Payment", xl("Confirm Payment")); diff --git a/interface/billing/era_payments.php b/interface/billing/era_payments.php index 581afe4ba46b..7cbbf054cd45 100644 --- a/interface/billing/era_payments.php +++ b/interface/billing/era_payments.php @@ -19,9 +19,6 @@ require_once("../globals.php"); -require_once("$srcdir/patient.inc.php"); -require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->get('OE_SITE_DIR') . "/statement.inc.php"); -require_once("$srcdir/options.inc.php"); use OpenEMR\Billing\ParseERA; use OpenEMR\Billing\SLEOB; @@ -33,6 +30,10 @@ use OpenEMR\Core\OEGlobalsBag; use OpenEMR\OeUI\OemrUI; +require_once(OEGlobalsBag::getInstance()->getSrcDir() . '/patient.inc.php'); +require_once(OEGlobalsBag::getInstance()->getString('OE_SITE_DIR') . '/statement.inc.php'); +require_once(OEGlobalsBag::getInstance()->getSrcDir() . '/options.inc.php'); + if (!AclMain::aclCheckCore('acct', 'bill', '', 'write') && !AclMain::aclCheckCore('acct', 'eob', '', 'write')) { AccessDeniedHelper::denyWithTemplate("ACL check failed for acct/bill or acct/eob: ERA Posting", xl("ERA Posting")); } @@ -306,7 +307,7 @@ function OnloadAction() // files they should be listed thereafter, please add _xpd suffix to the file name $arr_files_php = ["era_payments_xpd", "search_payments_xpd", "new_payment_xpd"]; $current_state = collectAndOrganizeExpandSetting($arr_files_php); - require_once("$srcdir/expand_contract_inc.php"); + require_once(OEGlobalsBag::getInstance()->getSrcDir() . '/expand_contract_inc.php'); ?> <?php echo xlt('ERA Posting'); ?>
oeBelowContainerDiv();?> - + oeBelowContainerDiv();?> - + "; ?> diff --git a/interface/modules/custom_modules/oe-module-faxsms/setup_voice.php b/interface/modules/custom_modules/oe-module-faxsms/setup_voice.php index 1bb9c0c0c0ef..f33d0167d39f 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/setup_voice.php +++ b/interface/modules/custom_modules/oe-module-faxsms/setup_voice.php @@ -14,6 +14,7 @@ require_once(__DIR__ . "/../../../globals.php"); +use OpenEMR\Common\Session\SessionWrapperFactory; use OpenEMR\Core\Header; use OpenEMR\Modules\FaxSMS\Controller\AppDispatch; @@ -26,6 +27,7 @@ } $c = $clientApp->getCredentials(); $module_config = $_REQUEST['module_config'] ?? 1; +$pid = SessionWrapperFactory::getInstance()->getActiveSession()->get('pid') ?? 0; echo ""; ?> diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php index 0396b0212dd3..af38b74ebf80 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php @@ -974,6 +974,7 @@ private function processMessageStoreList($messageStoreList, $serviceType): false $type = strtolower((string)$messageStore->type); $direction = strtolower((string)$messageStore->direction); $messageText = ''; + $pname = ''; if ($type === "sms" && $type === $serviceType) { if ($direction === "inbound") { $links = $this->generateSmsActionLinks($id, $uri, $messageStore->from->phoneNumber ?? ''); diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/Events/NotificationEventListener.php b/interface/modules/custom_modules/oe-module-faxsms/src/Events/NotificationEventListener.php index 09439b402de3..2c6641aa540c 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/Events/NotificationEventListener.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/Events/NotificationEventListener.php @@ -318,11 +318,7 @@ public function onNotifySendEvent(SendNotificationEvent $event): string $recipientPhone = $data['recipient_phone'] ?: $patient['phone']; $status = ''; - if (empty($data['alt_content'] ?? '')) { - xl("Please follow below link to complete the requested document."); - } else { - $message = $data['alt_content']; - } + $message = ($data['alt_content'] ?? '') ?: xl("Please follow below link to complete the requested document."); if ($patient['hipaa_allowsms'] == 'YES') { $clientApp = AppDispatch::getApiService('sms'); @@ -332,11 +328,7 @@ public function onNotifySendEvent(SendNotificationEvent $event): string $message, null // will get the "from" phone # from credentials ); - if ($status_api !== true) { - $status .= text($status_api); - } else { - $status .= xlt("Message sent."); - } + $status .= $status_api === true ? xlt("Message sent.") : text($status_api); } if (!empty($patient['email']) && ($data['include_email'] ?? false) && ($patient['hipaa_allowemail'] == 'YES')) { diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/RCVoice/VoiceFunctionsTrait.php b/interface/modules/custom_modules/oe-module-faxsms/src/RCVoice/VoiceFunctionsTrait.php index 8e4aa53377bd..6a24e3325cc6 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/RCVoice/VoiceFunctionsTrait.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/RCVoice/VoiceFunctionsTrait.php @@ -139,6 +139,7 @@ public function getVoicemailAttachment(string $messageId): string|false public function install() { + $response = null; try { $session = SessionWrapperFactory::getInstance()->getActiveSession(); $token = $session->get('ringcentral_voice_token') ?? 'changeme'; diff --git a/interface/modules/custom_modules/oe-module-weno/openemr.bootstrap.php b/interface/modules/custom_modules/oe-module-weno/openemr.bootstrap.php index 3bb6d9831453..5288c92aae6a 100644 --- a/interface/modules/custom_modules/oe-module-weno/openemr.bootstrap.php +++ b/interface/modules/custom_modules/oe-module-weno/openemr.bootstrap.php @@ -14,13 +14,13 @@ namespace OpenEMR\Modules\WenoModule; /** - * @global OpenEMR\Core\ModulesClassLoader $classLoader + * @var \OpenEMR\Core\ModulesClassLoader $classLoader */ $classLoader->registerNamespaceIfNotExists('OpenEMR\\Modules\\WenoModule\\', __DIR__ . DIRECTORY_SEPARATOR . 'src'); /** - * @global EventDispatcherInterface $eventDispatcher Injected by the OpenEMR module loader; + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher Injected by the OpenEMR module loader; */ $bootstrap = new Bootstrap($eventDispatcher); diff --git a/interface/modules/custom_modules/oe-module-weno/src/Services/LogImportBuild.php b/interface/modules/custom_modules/oe-module-weno/src/Services/LogImportBuild.php index 0dbe962f10b3..49a5fc2660a7 100644 --- a/interface/modules/custom_modules/oe-module-weno/src/Services/LogImportBuild.php +++ b/interface/modules/custom_modules/oe-module-weno/src/Services/LogImportBuild.php @@ -91,6 +91,7 @@ public function buildPrescriptionInserts(): bool|string if (!isset($line[1])) { continue; } + $is_saved = 0; if (isset($line[4])) { $this->messageid = $line[4]; $is_saved = $this->checkMessageId(); diff --git a/interface/modules/custom_modules/oe-module-weno/templates/indexrx.php b/interface/modules/custom_modules/oe-module-weno/templates/indexrx.php index effc2fa8834e..6cffd8aa10ab 100644 --- a/interface/modules/custom_modules/oe-module-weno/templates/indexrx.php +++ b/interface/modules/custom_modules/oe-module-weno/templates/indexrx.php @@ -15,6 +15,7 @@ //header("Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-src *;", true); // Preserve CSP header for security require_once("../../../../globals.php"); +/** @var string $srcdir */ require_once("$srcdir/patient.inc.php"); use OpenEMR\BC\ServiceContainer; @@ -34,6 +35,7 @@ } $session = SessionWrapperFactory::getInstance()->getActiveSession(); +$pid = $session->get('pid') ?? ''; // Let's see if letting user decide to reset fly's! // We really don't need because we can do transparently but Weno requested so... $wenoValidate = new WenoValidate(); @@ -124,7 +126,12 @@ $(function () { const warnMsg = ""; asyncAlertMsg(warnMsg, 8000, 'danger', 'lg').then(() => { - window.location.href = "getWebRoot() ?>/interface/patient_file/summary/demographics.php?set_pid=get('pid') ?? $pid ?? '')) ?>"; + getWebRoot() + . '/interface/patient_file/summary/demographics.php?' + . http_build_query(['set_pid' => $session->get('pid') ?? $pid]); + ?> + window.location.href = ; }); }); diff --git a/interface/modules/custom_modules/oe-module-weno/templates/weno_users.php b/interface/modules/custom_modules/oe-module-weno/templates/weno_users.php index ca8e3a027524..4fc2f77fab92 100644 --- a/interface/modules/custom_modules/oe-module-weno/templates/weno_users.php +++ b/interface/modules/custom_modules/oe-module-weno/templates/weno_users.php @@ -32,6 +32,7 @@ $wenoLog = new WenoLogService(); $fetch = sqlStatement("SELECT id,username,lname,fname,weno_prov_id,facility,facility_id FROM `users` WHERE active = 1 AND `username` > ''"); +$usersData = []; while ($row = sqlFetchArray($fetch)) { $usersData[] = $row; } diff --git a/interface/modules/zend_modules/module/Acl/src/Acl/Controller/AclController.php b/interface/modules/zend_modules/module/Acl/src/Acl/Controller/AclController.php index 21a2dfc4d123..467f1db7d1fb 100644 --- a/interface/modules/zend_modules/module/Acl/src/Acl/Controller/AclController.php +++ b/interface/modules/zend_modules/module/Acl/src/Acl/Controller/AclController.php @@ -53,6 +53,7 @@ public function indexAction() $user_group_denied = $this->createUserGroups("user_group_denied_", "display:none;", "draggable4", "class='class_li'"); $result = $this->getAclTable()->getActiveModules(); + $array_active_modules = []; foreach ($result as $row) { $array_active_modules[$row['mod_id']] = $row['mod_name']; } diff --git a/interface/modules/zend_modules/module/Application/src/Application/Plugin/CommonPlugin.php b/interface/modules/zend_modules/module/Application/src/Application/Plugin/CommonPlugin.php index 7413a7a1e493..421550167442 100644 --- a/interface/modules/zend_modules/module/Application/src/Application/Plugin/CommonPlugin.php +++ b/interface/modules/zend_modules/module/Application/src/Application/Plugin/CommonPlugin.php @@ -125,6 +125,7 @@ public function getList($list_id, $selected = '', $opt = '') $this->listenerObject = new Listener(); $res = QueryUtils::fetchRecords("SELECT * FROM list_options WHERE list_id=? ORDER BY seq, title", [$list_id]); $i = 0; + $rows = []; if ($opt == 'search') { $rows[$i] = [ 'value' => 'all', diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php index eb7750eea074..fea95e7acd31 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php @@ -686,6 +686,7 @@ public function getEachCCDAComponentDetailsAction() '; foreach ($social_history_audit['social_history'] as $val) { $array_his_tobacco = explode("|", (string) $val['smoking']); + $his_tob_date = ''; if ($array_his_tobacco[2] != 0 && $array_his_tobacco[2] != '') { $his_tob_date = substr($array_his_tobacco[2], 0, 4) . "-" . substr($array_his_tobacco[2], 4, 2) . "-" . substr($array_his_tobacco[2], 6, 2); } diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php index 7cf320f32f67..251a9029c994 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php @@ -460,6 +460,7 @@ public function autosendAction() return; } + $combination = $this->params('pids'); $view = new ViewModel([ 'combination' => $combination, 'listenerObject' => $this->listenerObject, @@ -484,6 +485,7 @@ public function autosignoffAction() $date = date('Y-m-d', $str_time); $encounter = $this->getEncounterccdadispatchTable()->getEncounterDate($date); + $result = null; foreach ($encounter as $row) { $result = $this->getEncounterccdadispatchTable()->signOff($row['pid'], $row['encounter']); } diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php index c607af61e695..a802b99ba8fe 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php @@ -113,6 +113,8 @@ public function indexAction() || ($downloadqrda3_consolidated == 'download_qrda3_consolidated') ) { $pids = ''; + $combination = null; + $pid = null; if ($request->getQuery('pid_ccda')) { $pid = $request->getQuery('pid_ccda'); if ($pid != '') { diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php index b3b530a16967..7909786dfe91 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CarecoordinationTable.php @@ -1638,6 +1638,7 @@ public function insertApprovedData($data) $query_select = "SELECT * FROM list_options WHERE list_id = ? AND title = ?"; $result = QueryUtils::fetchRecords($query_select, ['outcome', $data['lists1-observation_text-con'][$i]]); + $o_id = null; if (count($result) > 0) { $q_update = "UPDATE list_options SET activity = 1 WHERE list_id = ? AND title = ? AND codes = ?"; QueryUtils::sqlStatementThrowException($q_update, ['outcome', $data['lists1-observation_text-con'][$i], 'SNOMED-CT:' . $data['lists1-observation-con'][$i]]); @@ -1687,6 +1688,7 @@ public function insertApprovedData($data) QueryUtils::sqlStatementThrowException($query4, [(null), $data['pid'], $data['lists1_exist-list_id'][$i]]); } } elseif (substr((string) $key, 0, -4) == 'lists2-con') { + $allergy_begdate_value = null; if (!empty($data['lists2-begdate-con'][$i])) { $allergy_begdate_value = ApplicationTable::fixDate($data['lists2-begdate-con'][$i], 'yyyy-mm-dd', 'dd/mm/yyyy'); } elseif (empty($data['lists2-begdate-con'][$i])) { @@ -1745,8 +1747,9 @@ public function insertApprovedData($data) ?, 1 )"; - if ($value['reaction_text']) { - QueryUtils::sqlStatementThrowException($q_insert_units_option, [$reaction_option_id, $data['lists2-reaction_text-con'][$i]]); + $reactionText = $data['lists2-reaction_text-con'][$i] ?? ''; + if ($reactionText !== '') { + QueryUtils::sqlStatementThrowException($q_insert_units_option, [$reaction_option_id, $reactionText]); } } @@ -1792,6 +1795,7 @@ public function insertApprovedData($data) FROM users WHERE npi=?";// abook_type='external_provider' AND $res_query_sel_users = QueryUtils::fetchRecords($query_sel_users, [$data['lists3-provider_npi-con'][$i]]); + $provider_id = null; if (count($res_query_sel_users) > 0) { foreach ($res_query_sel_users as $value1) { $provider_id = $value1['id']; diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaGenerator.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaGenerator.php index f1042f924a9d..d53ac366af35 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaGenerator.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaGenerator.php @@ -87,8 +87,9 @@ public function generate( if (!$sections) { $components0 = $this->getEncounterccdadispatchTable()->getCCDAComponents(0); + $str = ''; foreach ($components0 as $key => $value) { - if ($str ?? '') { + if ($str !== '') { $str .= '|'; } else { $str = $key; @@ -101,8 +102,9 @@ public function generate( if (!$components) { $components1 = $this->getEncounterccdadispatchTable()->getCCDAComponents(1); + $str1 = ''; foreach ($components1 as $key => $value) { - if ($str1 ?? '') { + if ($str1 !== '') { $str1 .= '|'; } else { $str1 = $key; diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncounterccdadispatchTable.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncounterccdadispatchTable.php index cd85a991882a..7c1dfc88153e 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncounterccdadispatchTable.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncounterccdadispatchTable.php @@ -1279,6 +1279,7 @@ public function getInformationRecipient($pid, $encounter, $recipients, $params) { $information_recipient = ''; $field_name = []; + $query = ''; $details = $this->getDetails('hie_recipient_id'); if ($recipients == 'hie') { @@ -1662,6 +1663,8 @@ public function getMedications($pid) $active = $row['active'] > 0 ? 'active' : 'completed'; + $start_date = ''; + $start_date_formatted = ''; if ($row['start_date']) { $start_date = str_replace('-', '', $row['start_date']); $start_date_formatted = \Application\Model\ApplicationTable::fixDate($row['start_date'], OEGlobalsBag::getInstance()->get('date_display_format'), 'yyyy-mm-dd');; @@ -2259,6 +2262,7 @@ public function getEncounterHistory($pid) $problem = ''; $primary_diagnosis = ''; $issue_codes = ''; + $encounter_diagnosis = ''; if (count($res_issues ?? []) > 0) { $i = 0; foreach ($res_issues as $issue) { @@ -3409,7 +3413,10 @@ public function getCarecoordinationProvenanceForField($field_name) */ public function getDetails($field_name): ?array { - if ($field_name == 'hie_custodian_id') { + if (!is_string($field_name) && !is_int($field_name)) { + return null; + } + if ($field_name === 'hie_custodian_id') { $query = "SELECT f.name AS organization, f.street, f.city, f.state, f.postal_code AS zip, f.phone as phonew1, f.uuid, f.oid AS facility_oid, f.facility_npi FROM facility AS f JOIN modules AS mo ON mo.mod_directory='Carecoordination' @@ -3754,6 +3761,7 @@ public function signOff($pid, $encounter) /*Saving Demographics to locked data*/ $query_patient_data = "SELECT * FROM patient_data WHERE pid = ?"; $result_patient_data = QueryUtils::fetchRecords($query_patient_data, [$pid]); + $row_patient_data = []; foreach ($result_patient_data as $row_patient_data) { } @@ -3770,6 +3778,7 @@ public function signOff($pid, $encounter) $query_saved_forms = "SELECT formid FROM combined_encountersaved_forms WHERE pid = ? AND encounter = ?"; $result_saved_forms = QueryUtils::fetchRecords($query_saved_forms, [$pid, $encounter]); $count = 0; + $forms = []; foreach ($result_saved_forms as $row_saved_forms) { $form_dir = ''; $form_type = 0; @@ -3837,6 +3846,7 @@ public function lockedthisform($pid, $encounter, $formdir, $formtype, $formid) { $query = "select count(*) as count from combination_form where pid = ? and encounter = ? and form_dir = ? and form_type = ? and form_id = ?"; $result = QueryUtils::fetchRecords($query, [$pid, $encounter, $formdir, $formtype, $formid]); + $count = ['count' => 0]; foreach ($result as $count) { } @@ -3984,6 +3994,7 @@ private function getMostRecentPatientReferral($pid) // this query is only true if the referral was inserted as part of the ccda generation process. This is code migrated from EncountermanagerTable $refs = QueryUtils::fetchRecords("select t.id as trans_id from transactions t where t.pid = ? and t.date = NOW() AND t.title = 'LBTref'", [$pid]); + $trans_id = null; if (count($refs) == 0) { // the choose the most recent transaction to link this up... This could create problems in the // future if multiple referrals are created BEFORE sending the CCDA. @@ -4056,6 +4067,7 @@ public function date_format($date, $format = null) $date = str_replace('/', '-', $date); $arr = explode('-', $date); + $formatted_date = $date; if ($format == 'm/d/y') { $formatted_date = $arr[1] . "/" . $arr[2] . "/" . $arr[0]; } diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncountermanagerTable.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncountermanagerTable.php index 19e863e05e12..0b04c06fb365 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncountermanagerTable.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/EncountermanagerTable.php @@ -153,6 +153,7 @@ public function date_format($date, $format) $date = str_replace('/', '-', $date); $arr = explode('-', $date); + $formatted_date = $date; if ($format == 'm/d/y') { $formatted_date = $arr[1] . "/" . $arr[2] . "/" . $arr[0]; } @@ -255,6 +256,7 @@ public function transmitCcdToRecipients($data = []) $verifyMessageReceivedChecked = OEGlobalsBag::getInstance()->getBoolean('phimail_verifyrecipientreceived_enable') ? true : false; + $elec_sent = []; try { foreach ($rec_arr as $recipient) { $elec_sent = []; @@ -262,6 +264,8 @@ public function transmitCcdToRecipients($data = []) foreach ($arr as $value) { $query = "SELECT id,transaction_id FROM ccda WHERE pid = ? ORDER BY id DESC LIMIT 1"; $result = QueryUtils::fetchRecords($query, [$value]); + $ccda_id = null; + $trans_id = null; // weird foreach loop considering the limit 1 up above? foreach ($result as $val) { $ccda_id = $val['id']; diff --git a/interface/modules/zend_modules/module/Ccr/src/Ccr/Model/CcrTable.php b/interface/modules/zend_modules/module/Ccr/src/Ccr/Model/CcrTable.php index 4ab251d73a48..c8a861c0b603 100644 --- a/interface/modules/zend_modules/module/Ccr/src/Ccr/Model/CcrTable.php +++ b/interface/modules/zend_modules/module/Ccr/src/Ccr/Model/CcrTable.php @@ -306,6 +306,7 @@ public function insertApprovedData($data) for ($i = 0; $i < count($val); $i++) { if ($val[$i] == 'insert') { if (substr((string) $key, 0, -4) == 'lists1') { + $activity = null; if ($data['lists1-activity'][$i] == 'Active') { $activity = 1; } elseif ($data['lists1-activity'][$i] == 'Inactive') { @@ -318,6 +319,7 @@ public function insertApprovedData($data) $query = "INSERT INTO lists (pid, date, type, title, diagnosis, reaction) VALUES (?,?,?,?,?,?)"; QueryUtils::sqlStatementThrowException($query, [$data['pid'], ApplicationTable::fixDate($data['lists2-date'][$i], 'yyyy-mm-dd', OEGlobalsBag::getInstance()->get('date_display_format')), $data['lists2-type'][$i], $data['lists2-title'][$i], $data['lists2-diagnosis'][$i], $data['lists2-reaction'][$i]]); } elseif (substr((string) $key, 0, -4) == 'prescriptions') { + $active = null; if ($data['prescriptions-active'][$i] == 'Active') { $active = 1; } elseif ($data['prescriptions-active'][$i] == 'Inactive') { @@ -332,6 +334,7 @@ public function insertApprovedData($data) } } elseif ($val[$i] == 'update') { if (substr((string) $key, 0, -4) == 'lists1') { + $activity = null; if ($data['lists1-activity'][$i] == 'Active') { $activity = 1; } elseif ($data['lists1-activity'][$i] == 'Inactive') { diff --git a/interface/modules/zend_modules/module/Immunization/src/Immunization/Controller/ImmunizationController.php b/interface/modules/zend_modules/module/Immunization/src/Immunization/Controller/ImmunizationController.php index 81de670fe28f..5d887cb377ea 100644 --- a/interface/modules/zend_modules/module/Immunization/src/Immunization/Controller/ImmunizationController.php +++ b/interface/modules/zend_modules/module/Immunization/src/Immunization/Controller/ImmunizationController.php @@ -171,6 +171,7 @@ public function getAllCodes($data) $defaultCode = $data['codes'] ?? ''; $res = $this->getImmunizationTable()->codeslist(); $i = 0; + $rows = []; foreach ($res as $value) { $select = $value == $defaultCode ? true : false; diff --git a/interface/modules/zend_modules/module/Installer/src/Installer/Controller/InstallerController.php b/interface/modules/zend_modules/module/Installer/src/Installer/Controller/InstallerController.php index 166e244955e4..94ba4852e6ac 100644 --- a/interface/modules/zend_modules/module/Installer/src/Installer/Controller/InstallerController.php +++ b/interface/modules/zend_modules/module/Installer/src/Installer/Controller/InstallerController.php @@ -541,6 +541,9 @@ function getModuleVersionFromFile($modId) $upgrade_sql = $ModulePath . "/sql/upgrade.sql"; $install_acl = $ModulePath . "/acl/acl_setup.php"; if (file_exists($version_of_module) && (file_exists($table_sql) || file_exists($install_sql) || file_exists($install_acl))) { + $v_major = '0'; + $v_minor = '0'; + $v_patch = '0'; include_once($version_of_module); $version = $v_major . "." . $v_minor . "." . $v_patch; return $version; @@ -733,6 +736,7 @@ public function UpgradeModuleSQL($modId = '') $add_query_string = 0; $add_ended_divs = 0; $k = 0; + $curr_html_tag = false; foreach ($matches[1] as $string) { $prev_html_tag = false; if (preg_match("/<([a-z]+).*?>([^<]+)<\/([a-z]+)>/i", $string, $mm)) { @@ -841,6 +845,7 @@ public function InstallModule($modId = '', $mod_enc_menu = '', $mod_nick_name = $modType = $registryEntry->type; $dirModule = $registryEntry->modDirectory; $sqlInstalled = false; + $status = $this->listenerObject->z_xlt("Failure"); if ($modType == InstModuleTable::MODULE_TYPE_CUSTOM) { $fullDirectory = OEGlobalsBag::getInstance()->getSrcDir() . "/../" . OEGlobalsBag::getInstance()->get('baseModDir') . OEGlobalsBag::getInstance()->get('customModDir') . "/" . $dirModule; if ($this->InstallerTable->installSQL($modId, $modType, $fullDirectory)) { diff --git a/interface/modules/zend_modules/module/Installer/src/Installer/Model/InstModuleTable.php b/interface/modules/zend_modules/module/Installer/src/Installer/Model/InstModuleTable.php index 55fae438f8a5..4a71ca1b0c13 100644 --- a/interface/modules/zend_modules/module/Installer/src/Installer/Model/InstModuleTable.php +++ b/interface/modules/zend_modules/module/Installer/src/Installer/Model/InstModuleTable.php @@ -740,11 +740,11 @@ public function checkDependencyOnDisable($mod_id) public function getDependencyModules($mod_id) { $modDirname = $this->getModuleDirectory($mod_id); + $ret_str = ""; if ($modDirname <> "") { $depModuleStatusArr = []; //GET DEPENDED MODULES OF A MODULE HOOKS FROM A FUNCTION IN ITS MODEL CONFIGURATION CLASS $depModulesArr = $this->getDependedModulesByDirectoryName($modDirname); - $ret_str = ""; if (count($depModulesArr) > 0) { $count = 0; foreach ($depModulesArr as $modDir) { diff --git a/interface/modules/zend_modules/module/PatientFilter/acl/acl_setup.php b/interface/modules/zend_modules/module/PatientFilter/acl/acl_setup.php index 9810e6678ea7..33454b4d1b8f 100644 --- a/interface/modules/zend_modules/module/PatientFilter/acl/acl_setup.php +++ b/interface/modules/zend_modules/module/PatientFilter/acl/acl_setup.php @@ -1,5 +1,6 @@ 0]; if ($yearly_limit > 0) { # check to see if screens are within the current year. $lastyear = date("Y-m-d", strtotime("-1 year", strtotime(date("Y-m-d H:i:s")))); diff --git a/interface/modules/zend_modules/module/PrescriptionTemplates/src/PrescriptionTemplates/Controller/PrescriptionTemplatesController.php b/interface/modules/zend_modules/module/PrescriptionTemplates/src/PrescriptionTemplates/Controller/PrescriptionTemplatesController.php index 1b17075def23..cc11c1e2620d 100644 --- a/interface/modules/zend_modules/module/PrescriptionTemplates/src/PrescriptionTemplates/Controller/PrescriptionTemplatesController.php +++ b/interface/modules/zend_modules/module/PrescriptionTemplates/src/PrescriptionTemplates/Controller/PrescriptionTemplatesController.php @@ -34,6 +34,7 @@ protected function getDefaultTemplate($id) { $ids = preg_split('/::/', substr((string) $id, 1, strlen((string) $id) - 2), -1, PREG_SPLIT_NO_EMPTY); $prescriptions = []; + $p = null; foreach ($ids as $id) { $p = new \Prescription($id); @@ -43,7 +44,7 @@ protected function getDefaultTemplate($id) $prescriptions[$p->provider->id][] = $p; } - $patient = $p->patient; + $patient = $p?->patient; $session = SessionWrapperFactory::getInstance()->getActiveSession(); $defaultHtml = new ViewModel(['patient' => $patient, 'prescriptions' => $prescriptions, 'langDir' => $session->get('language_direction')]); diff --git a/interface/modules/zend_modules/module/Syndromicsurveillance/src/Syndromicsurveillance/Model/SyndromicsurveillanceTable.php b/interface/modules/zend_modules/module/Syndromicsurveillance/src/Syndromicsurveillance/Model/SyndromicsurveillanceTable.php index dec4650c1d47..0969e44546e5 100644 --- a/interface/modules/zend_modules/module/Syndromicsurveillance/src/Syndromicsurveillance/Model/SyndromicsurveillanceTable.php +++ b/interface/modules/zend_modules/module/Syndromicsurveillance/src/Syndromicsurveillance/Model/SyndromicsurveillanceTable.php @@ -386,6 +386,7 @@ function generate_hl7($fromDate, $toDate, $code_selected, $provider_selected, $s foreach ($o_result as $row) { $i++; if ($row['code'] == 'SS003') { + $text = ''; if ($row['ob_value'] == '261QE0002X') { $text = 'Emergency Care'; } elseif ($row['ob_value'] == '261QM2500X') { @@ -509,6 +510,7 @@ public function date_format($date, $format) $date = str_replace('/', '-', $date); $arr = explode('-', $date); + $formatted_date = $date; if ($format == 'm/d/y') { $formatted_date = $arr[1] . "/" . $arr[2] . "/" . $arr[0]; } diff --git a/interface/orders/list_reports.php b/interface/orders/list_reports.php index 950cde8bb5e3..2947431a7be9 100644 --- a/interface/orders/list_reports.php +++ b/interface/orders/list_reports.php @@ -22,7 +22,7 @@ require_once("../globals.php"); require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/patient.inc.php"); require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"); -$includeRoot = \OpenEMR\Core\OEGlobalsBag::getInstance()->getString('include_root'); +$includeRoot = \OpenEMR\Core\OEGlobalsBag::getInstance()->getIncludeRoot(); if (file_exists("$includeRoot/procedure_tools/quest/QuestResultClient.php")) { require_once("$includeRoot/procedure_tools/quest/QuestResultClient.php"); } diff --git a/interface/reports/amc_full_report.php b/interface/reports/amc_full_report.php index 523bc07f63f8..91906b0dbd25 100644 --- a/interface/reports/amc_full_report.php +++ b/interface/reports/amc_full_report.php @@ -1,8 +1,8 @@ getSrcDir() . "/options.inc.php"; +require_once \OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/report_database.inc.php"; // TODO: @adunsulag we need to move ALL this AMC stuff into a namespace. Any AMC classes should use autoloader not requires. require_once("../../library/classes/rulesets/library/RsReportIF.php"); require_once("../../library/classes/rulesets/library/RsUnimplementedIF.php"); diff --git a/interface/reports/amc_tracking.php b/interface/reports/amc_tracking.php index b20a11b51bd7..19acc4228178 100644 --- a/interface/reports/amc_tracking.php +++ b/interface/reports/amc_tracking.php @@ -12,8 +12,8 @@ require_once("../globals.php"); require_once("../../library/patient.inc.php"); -require_once "$srcdir/options.inc.php"; -require_once "$srcdir/amc.php"; +require_once \OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"; +require_once \OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/amc.php"; use OpenEMR\Common\Acl\AccessDeniedHelper; use OpenEMR\Common\Acl\AclMain; diff --git a/interface/reports/appointments_report.php b/interface/reports/appointments_report.php index 352b53d940ae..68ce160366c8 100644 --- a/interface/reports/appointments_report.php +++ b/interface/reports/appointments_report.php @@ -30,9 +30,10 @@ /** * @global $srcdir */ -require_once "$srcdir/options.inc.php"; -require_once "$srcdir/appointments.inc.php"; -require_once "$srcdir/clinical_rules.php"; +$srcDir = \OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir(); +require_once $srcDir . '/options.inc.php'; +require_once $srcDir . '/appointments.inc.php'; +require_once $srcDir . '/clinical_rules.php'; use OpenEMR\Common\{ Acl\AccessDeniedHelper, @@ -134,6 +135,7 @@ function appointments_fetch_reminders($pid, $appt_date): array $seq_due = []; $seq_cat = []; $seq_act = []; + $rems_out = []; foreach ($rems as $ix => $rem) { $rem_out = []; $rule_txt = fetch_rule_txt('rule_reminder_due_opt', $rem['due_status']); diff --git a/interface/reports/appt_encounter_report.php b/interface/reports/appt_encounter_report.php index 56db9c2ea2dd..f252e244c221 100644 --- a/interface/reports/appt_encounter_report.php +++ b/interface/reports/appt_encounter_report.php @@ -29,9 +29,11 @@ */ require_once("../globals.php"); -require_once("$srcdir/patient.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/patient.inc.php"); require_once("../../custom/code_types.inc.php"); +/** @var array> $code_types */ + use OpenEMR\Billing\BillingUtilities; use OpenEMR\Common\Acl\AccessDeniedHelper; use OpenEMR\Common\Acl\AclMain; @@ -110,6 +112,7 @@ function endDoctor(&$docrow): void $form_facility = $_POST['form_facility'] ?? ''; $form_from_date = (isset($_POST['form_from_date'])) ? DateToYYYYMMDD($_POST['form_from_date']) : date('Y-m-d'); $form_to_date = (isset($_POST['form_to_date'])) ? DateToYYYYMMDD($_POST['form_to_date']) : date('Y-m-d'); +$res = null; if (!empty($_POST['form_refresh'])) { // MySQL doesn't grok full outer joins so we do it the hard way. // @@ -498,12 +501,6 @@ function endDoctor(&$docrow): void  '; - echo substr($row['pc_startTime'], 0, 5); - } - *****************************************************************/ if (empty($row['pc_eventDate'])) { echo text(oeFormatShortDate(substr((string) $row['encdate'], 0, 10))); } else { diff --git a/interface/reports/audit_log_tamper_report.php b/interface/reports/audit_log_tamper_report.php index 1cd29b32cb0e..3bcd7b1a1378 100644 --- a/interface/reports/audit_log_tamper_report.php +++ b/interface/reports/audit_log_tamper_report.php @@ -205,6 +205,7 @@ function setpatient(pid, lname, fname, dob) { $type_event = ""; $tevent = ""; $gev = ""; + $getevent = ""; if ($eventname != "" && $type_event != "") { $getevent = $eventname . "-" . $type_event; } @@ -248,6 +249,7 @@ function setpatient(pid, lname, fname, dob) { } $checkSumOldApi = $iter['checksum_api']; + $checkSumNewApi = ''; if (!empty($checkSumOldApi)) { $checkSumNewApi = hash('sha3-512', $iter['log_id_api'] . $iter['user_id'] . $iter['patient_id_api'] . $iter['ip_address'] . $iter['method'] . $iter['request'] . $iter['request_url'] . $iter['request_body'] . $iter['response'] . $iter['created_time']); } diff --git a/interface/reports/cdr_log.php b/interface/reports/cdr_log.php index f54f674cf944..0433bd122411 100644 --- a/interface/reports/cdr_log.php +++ b/interface/reports/cdr_log.php @@ -12,8 +12,8 @@ require_once("../globals.php"); require_once("../../library/patient.inc.php"); -require_once "$srcdir/options.inc.php"; -require_once "$srcdir/clinical_rules.php"; +require_once \OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"; +require_once \OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/clinical_rules.php"; use OpenEMR\BC\ServiceContainer; use OpenEMR\ClinicalDecisionRules\Interface\ControllerRouter; diff --git a/interface/reports/chart_location_activity.php b/interface/reports/chart_location_activity.php index a518b9122b93..fee8da2086f0 100644 --- a/interface/reports/chart_location_activity.php +++ b/interface/reports/chart_location_activity.php @@ -13,8 +13,8 @@ */ require_once("../globals.php"); -require_once("$srcdir/patient.inc.php"); -require_once("$srcdir/options.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/patient.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"); use OpenEMR\Common\Csrf\CsrfUtils; use OpenEMR\Common\Session\SessionWrapperFactory; @@ -73,7 +73,7 @@ - getActiveSession()->get('pid'); $ptrow = []; if (!empty($form_patient_id)) { $query = "SELECT pid, pubpid, fname, mname, lname FROM patient_data WHERE " . diff --git a/interface/reports/charts_checked_out.php b/interface/reports/charts_checked_out.php index 11ca3d078e08..7ae8bc1cdf84 100644 --- a/interface/reports/charts_checked_out.php +++ b/interface/reports/charts_checked_out.php @@ -13,7 +13,7 @@ */ require_once("../globals.php"); -require_once("$srcdir/patient.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/patient.inc.php"); use OpenEMR\Core\Header; use OpenEMR\Services\PatientService; diff --git a/interface/reports/clinical_reports.php b/interface/reports/clinical_reports.php index 34764cf83b67..7b6bef450e27 100644 --- a/interface/reports/clinical_reports.php +++ b/interface/reports/clinical_reports.php @@ -12,8 +12,8 @@ */ require_once("../globals.php"); -require_once("$srcdir/patient.inc.php"); -require_once("$srcdir/options.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/patient.inc.php"); +require_once(\OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"); require_once("../drugs/drugs.inc.php"); require_once("../../custom/code_types.inc.php"); @@ -496,7 +496,7 @@ function submitForm() { c.code_text as code_text, fe.encounter as encounter, b.date as date"; - $mh_stmt .= ",code,code_text,encounter,date"; + $mh_stmt = ($mh_stmt ?? '') . ",code,code_text,encounter,date"; } if (strlen($form_immunization) > 0) { diff --git a/interface/reports/collections_report.php b/interface/reports/collections_report.php index 4aa8d4781938..f22581a9c10a 100644 --- a/interface/reports/collections_report.php +++ b/interface/reports/collections_report.php @@ -23,7 +23,7 @@ require_once("../globals.php"); require_once("../../library/patient.inc.php"); -require_once "$srcdir/options.inc.php"; +require_once \OpenEMR\Core\OEGlobalsBag::getInstance()->getSrcDir() . "/options.inc.php"; use OpenEMR\Billing\InvoiceSummary; use OpenEMR\Billing\SLEOB; @@ -61,6 +61,7 @@ $is_ageby_lad = str_contains(($_POST['form_ageby'] ?? ''), 'Last'); $form_facility = $_POST['form_facility'] ?? null; $form_provider = $_POST['form_provider'] ?? null; +$provider_name = ''; $form_payer_id = $_POST['form_payer_id'] ?? null; // reposition the page after closing invoice variables $form_page_y = $_POST['form_page_y'] ?? ''; @@ -622,6 +623,7 @@ function checkAll(checked) { echo " " . xlt('Custom') . "\n"; foreach ($arr_report as $key => $value) { echo "