Problem
If a generated module declares openemr/openemr as a require-dev entry in its root composer.json (the current template default), then in any dev environment built with a full composer install:
vendor/openemr/openemr/ lands on disk under the module's vendor tree.
- The module's
vendor/autoload.php registers a PSR-4 mapping OpenEMR\\ → its vendor's src/, alongside OpenEMR core's identical mapping.
- At runtime,
OpenEMR\… class lookups can resolve to the module's vendor copy of (e.g.) OpenEMR\Common\Acl\AclMain. Several OpenEMR classes call require_once(__DIR__ . '/../../../library/lists.inc.php'); — __DIR__ then resolves into the module's vendor tree and the same procedural file is loaded under a different absolute path than OpenEMR core already loaded it.
require_once dedups by string path, not by content, so the procedural function definitions get re-run → fatal Cannot redeclare collect_issue_type_category() (or any other function in library/*.inc.php) on patient demographics and other in-chart pages.
Production deploys are not affected (they use composer install --no-dev and the shadow vendor never exists), so this is purely a local-dev / test trap. But the fatal blocks essentially every chart workflow during development. We hit it in oce-module-sinch-conversations and fixed it in openCoreEMR/oce-module-sinch-conversations#119 (closes openCoreEMR/oce-module-sinch-conversations#118).
Proposal
Adopt the same fix in this template so newly-generated modules don't inherit the trap. The recipe:
- Remove
openemr/openemr from root composer.json (require-dev and repositories).
- Add
tools/openemr/composer.json (sub-composer) requiring openemr/openemr with the existing constraints + repositories. Gitignore its vendor/ and composer.lock.
phpstan.neon: add bootstrapFiles: [tools/openemr/vendor/autoload.php] so PHPStan still resolves OpenEMR\… symbols. Also add tools/openemr/vendor (?) to excludePaths.
composer phpstan script should self-install the sub-vendor if tools/openemr/vendor/autoload.php is missing — otherwise a fresh checkout that ran only composer install hits a confusing missing-file error.
compose.yml (if the template ships one for local dev): bind-mount from ./tools/openemr/vendor/openemr/openemr instead of ./vendor/openemr/openemr.
Taskfile.yml: new tools:install task; have openemr:prebuild (or whatever does npm install inside OpenEMR) deps: on it; let setup reach it transitively. Update vendor:clean to also clean tools/openemr/vendor/.
- CI (
.github/workflows/phpstan.yml): install + cache tools/openemr/vendor before running PHPStan. Cache key off composer.json files, since composer.lock is gitignored.
- Module CLI's path discovery (in
bin/install-module.php / AbstractModuleCommand::findOpenEmrPath): also look under __DIR__ . '/../../../../tools/openemr/vendor/openemr/openemr' so host-run module commands still find OpenEMR without --openemr-path.
- Test bootstrap: replace any reliance on the runtime root vendor providing OpenEMR types with explicit mocks under
tests/Mocks/. The sinch-conversations PR added stubs for OEGlobalsBag, GlobalsInitializedEvent, PatientCreatedEvent, PatientUpdatedEvent, MenuEvent, GlobalsService, GlobalSetting — the template likely needs an analogous (probably smaller) starter set.
- Docs: a short "why is OpenEMR under
tools/openemr/?" note in the README (or CLAUDE.md) so a future contributor doesn't move it back.
Reference
The full set of changes for sinch-conversations is in openCoreEMR/oce-module-sinch-conversations#119 — it can be diffed directly to extract the template-applicable pieces. Out of scope for this template issue: rolling the same pattern into other already-existing OCE modules (each gets its own per-module follow-up).
Problem
If a generated module declares
openemr/openemras arequire-deventry in its rootcomposer.json(the current template default), then in any dev environment built with a fullcomposer install:vendor/openemr/openemr/lands on disk under the module's vendor tree.vendor/autoload.phpregisters a PSR-4 mappingOpenEMR\\→ its vendor'ssrc/, alongside OpenEMR core's identical mapping.OpenEMR\…class lookups can resolve to the module's vendor copy of (e.g.)OpenEMR\Common\Acl\AclMain. Several OpenEMR classes callrequire_once(__DIR__ . '/../../../library/lists.inc.php');—__DIR__then resolves into the module's vendor tree and the same procedural file is loaded under a different absolute path than OpenEMR core already loaded it.require_oncededups by string path, not by content, so the procedural function definitions get re-run → fatalCannot redeclare collect_issue_type_category()(or any other function inlibrary/*.inc.php) on patient demographics and other in-chart pages.Production deploys are not affected (they use
composer install --no-devand the shadow vendor never exists), so this is purely a local-dev / test trap. But the fatal blocks essentially every chart workflow during development. We hit it inoce-module-sinch-conversationsand fixed it in openCoreEMR/oce-module-sinch-conversations#119 (closes openCoreEMR/oce-module-sinch-conversations#118).Proposal
Adopt the same fix in this template so newly-generated modules don't inherit the trap. The recipe:
openemr/openemrfrom rootcomposer.json(require-devandrepositories).tools/openemr/composer.json(sub-composer) requiringopenemr/openemrwith the existing constraints + repositories. Gitignore itsvendor/andcomposer.lock.phpstan.neon: addbootstrapFiles: [tools/openemr/vendor/autoload.php]so PHPStan still resolvesOpenEMR\…symbols. Also addtools/openemr/vendor (?)toexcludePaths.composer phpstanscript should self-install the sub-vendor iftools/openemr/vendor/autoload.phpis missing — otherwise a fresh checkout that ran onlycomposer installhits a confusing missing-file error.compose.yml(if the template ships one for local dev): bind-mount from./tools/openemr/vendor/openemr/openemrinstead of./vendor/openemr/openemr.Taskfile.yml: newtools:installtask; haveopenemr:prebuild(or whatever doesnpm installinside OpenEMR)deps:on it; letsetupreach it transitively. Updatevendor:cleanto also cleantools/openemr/vendor/..github/workflows/phpstan.yml): install + cachetools/openemr/vendorbefore running PHPStan. Cache key offcomposer.jsonfiles, sincecomposer.lockis gitignored.bin/install-module.php/AbstractModuleCommand::findOpenEmrPath): also look under__DIR__ . '/../../../../tools/openemr/vendor/openemr/openemr'so host-run module commands still find OpenEMR without--openemr-path.tests/Mocks/. The sinch-conversations PR added stubs forOEGlobalsBag,GlobalsInitializedEvent,PatientCreatedEvent,PatientUpdatedEvent,MenuEvent,GlobalsService,GlobalSetting— the template likely needs an analogous (probably smaller) starter set.tools/openemr/?" note in the README (orCLAUDE.md) so a future contributor doesn't move it back.Reference
The full set of changes for sinch-conversations is in openCoreEMR/oce-module-sinch-conversations#119 — it can be diffed directly to extract the template-applicable pieces. Out of scope for this template issue: rolling the same pattern into other already-existing OCE modules (each gets its own per-module follow-up).