Skip to content

Conversation

@rollingventures
Copy link

@rollingventures rollingventures commented Jan 19, 2026

Summary

This branch adds a configuration abstraction layer to the OpenCoreEMR module template, enabling flexible configuration from either database-backed globals or environment variables.

Key additions:

  • Configuration abstraction (ConfigAccessorInterface, ConfigFactory, EnvironmentConfigAccessor, GlobalConfig) - Allows modules to be configured via environment variables in containerized deployments while maintaining traditional database configuration support
  • Module access guard (ModuleAccessGuard) - Protects module endpoints by checking if the module is registered, enabled, and its settings allow access (returns 404 to avoid leaking module presence)
  • Exception hierarchy - Complete set of typed exceptions (NotFoundException, ValidationException, AccessDeniedException, etc.) with HTTP status codes
  • Sample controller and templates - Working example implementation demonstrating the patterns
  • Test mocks - MockEnvironmentConfigAccessor and MockGlobalsAccessor for unit testing

Changes: 30 files, +1,345 / -49 lines

Commits

  1. feat: add configuration abstraction layer
  2. feat: add module access guard
  3. feat: add exception hierarchy
  4. feat: add sample controller and templates
  5. feat: update Bootstrap with configuration factory pattern
  6. test: add configuration accessor mock classes
  7. docs: document configuration abstraction layer and patterns
  8. chore: remove placeholder gitkeep files
  9. chore: update copyright dates to 2026

DRAFT: One pass using LLM's with some manual review.

@rollingventures rollingventures changed the title chore/add configuration abstraction layer feat: add configuration abstraction layer Jan 19, 2026
@rollingventures rollingventures changed the title feat: add configuration abstraction layer feat: add configuration abstraction layer and module patterns Jan 19, 2026
@rollingventures rollingventures changed the title feat: add configuration abstraction layer and module patterns feat: add configuration abstraction layer Jan 19, 2026
Chris Dickman and others added 9 commits January 21, 2026 18:26
Add flexible configuration system supporting both database-backed
(OpenEMR globals) and environment variable configurations.

- ConfigAccessorInterface: Common interface for typed config access
- GlobalsAccessor: Now implements ConfigAccessorInterface
- EnvironmentConfigAccessor: Reads config from environment variables
- ConfigFactory: Selects accessor based on ENV_CONFIG env var
- GlobalConfig: Centralized wrapper with typed getters

Set {VENDOR_PREFIX}_{MODULENAME}_ENV_CONFIG=1 to enable env mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ModuleAccessGuard to prevent access to module endpoints when:
- Module is not registered in OpenEMR
- Module is disabled in module management
- Module's own 'enabled' setting is off

Returns 404 (not 403) to avoid leaking module presence.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add structured exception classes with HTTP status codes:

- {ModuleName}ExceptionInterface: Base interface with getStatusCode()
- {ModuleName}Exception: Abstract base class
- {ModuleName}NotFoundException: 404
- {ModuleName}UnauthorizedException: 401
- {ModuleName}AccessDeniedException: 403
- {ModuleName}ValidationException: 400
- {ModuleName}ConfigurationException: 500
- {ModuleName}ApiException: 502

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add working example implementation demonstrating module patterns:

- ExampleController: Shows dispatch pattern, CSRF validation, exceptions
- public/index.php: Entry point with guard, error handling
- templates/example/list.html.twig: List view with form
- templates/example/view.html.twig: Detail view

Demonstrates Twig filters (xlt, text, attr) and Bootstrap styling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update Bootstrap to use ConfigAccessorInterface injection:

- Accept optional ConfigAccessorInterface in constructor
- Use ConfigFactory to determine config source if not provided
- Add env config mode handling in global settings section
- Show informational message when config managed externally
- Add getExampleController() factory method

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add mock implementations for testing configuration:

- MockGlobalsAccessor: Mock for database config testing
- MockEnvironmentConfigAccessor: Mock for env config testing
- Update tests/bootstrap.php to load new mocks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update CLAUDE.md with comprehensive documentation:

- Add Configuration Abstraction Layer section
- Document environment variable mode usage
- Add Module Access Guard section
- Update file structure to include new files
- Update public entry point pattern with guard and error handling
- Update Bootstrap pattern with ConfigAccessorInterface
- Add env config mode handling in admin UI example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove .gitkeep files now that directories contain actual files:
- public/.gitkeep (replaced by index.php)
- templates/.gitkeep (replaced by example/ templates)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update all copyright headers from 2025 to 2026 across:
- LICENSE
- DEVELOPMENT.md
- PHP source files
- openemr.bootstrap.php
- version.php

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@kojiromike kojiromike force-pushed the chore/add-configuration-abstraction-layer branch from 5003ae2 to ae0ee87 Compare January 21, 2026 23:26
Copy link
Contributor

@kojiromike kojiromike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of good things here and this is definitely the right direction. Thanks for putting this together. I left some comments with the theme of making static analysis easier and doing less in the global space.

// Redirect back to list
return new Response('', Response::HTTP_FOUND, [
'Location' => $_SERVER['PHP_SELF']
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per CLAUDE.md guidelines, use RedirectResponse instead of manually setting Location header:

Suggested change
]);
return new RedirectResponse($_SERVER['PHP_SELF']);

This also requires adding the import:

use Symfony\Component\HttpFoundation\RedirectResponse;

namespace {VendorName}\Modules\{ModuleName}\Controller;

use {VendorName}\Modules\{ModuleName}\Exception\{ModuleName}AccessDeniedException;
use {VendorName}\Modules\{ModuleName}\Exception\{ModuleName}NotFoundException;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unused - {ModuleName}NotFoundException only appears in a comment (line 90). Consider removing to avoid unused import warnings after template instantiation, or add an actual usage example.

*/
private function showView(): Response
{
$id = $_GET['id'] ?? null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The controller accesses $_GET and $_POST directly here and in handleCreate(). The CLAUDE.md pattern shows dispatch(string $action, array $params) where params are passed in from the entry point. This would decouple the controller from superglobals and improve testability.

Consider:

public function dispatch(string $action, array $params = []): Response

Then pass $_REQUEST or $_GET + $_POST from public/index.php.

private const KEY_MAP = [
GlobalConfig::CONFIG_OPTION_ENABLED => '{VENDOR_PREFIX}_{MODULENAME}_ENABLED',
// Add your config option mappings here
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The KEY_MAP only maps CONFIG_OPTION_ENABLED, but GlobalConfig.php comments mention API_KEY and API_SECRET as examples. Consider adding commented placeholder mappings to match:

Suggested change
];
// Add your config option mappings here
// GlobalConfig::CONFIG_OPTION_API_KEY => '{VENDOR_PREFIX}_{MODULENAME}_API_KEY',
// GlobalConfig::CONFIG_OPTION_API_SECRET => '{VENDOR_PREFIX}_{MODULENAME}_API_SECRET',

* @license GNU General Public License 3
*/

namespace {VendorName}\Modules\{ModuleName}\Controller;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
namespace {VendorName}\Modules\{ModuleName}\Controller;
namespace OpenCoreEMR\Modules\{ModuleName}\Controller;

For our purposes this will always be the case. If someone else uses this public repo they can change their vendor name themselves.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these exception types

/**
* Get the HTTP status code for this exception
*/
public function getStatusCode(): int;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not actually sure every module exception implies a status code. The requirement that there be one seems to me to be the domain of a subtype of module exception. Anyway, I don't object to this interface existing, but I think the most abstract base exception type for the module shouldn't implement this.

* Abstraction for configuration access, matching Symfony ParameterBag's typed accessor methods.
* Allows configuration to be read from either OpenEMR globals (database) or environment variables.
*/
interface ConfigAccessorInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getKernel, perhaps...

</div>
</div>

<script src="{{ webroot|attr }}/public/assets/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in with a defer attribute, no?

</div>
</div>

<script src="{{ webroot|attr }}/public/assets/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in head with a defer?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants