Skip to content

Commit cecd310

Browse files
committed
feat: add ConfigService and GlobalsRegistrar helpers
Extract two patterns duplicated across every OCE module into this shared library: - ConfigService: upsert settings to the globals table via saveSetting() and saveEncryptedSetting() (CryptoGen). Uses portable ON DUPLICATE KEY UPDATE with bound parameters for MariaDB/MySQL compatibility. - GlobalsRegistrar + GlobalsSectionDescriptor: register a module's globals section (enable/disable toggle + settings page link) via GlobalsInitializedEvent, replacing identical boilerplate in every module bootstrap. Also adds openemr/openemr as a dev dependency (package repo pattern) and symfony/event-dispatcher as a runtime dependency. Closes #6 Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4cb30a5 commit cecd310

12 files changed

Lines changed: 512 additions & 1 deletion

.composer-require-checker.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
"callable",
1515
"iterable",
1616
"void",
17-
"object"
17+
"object",
18+
"attr",
19+
"xlt",
20+
"OpenEMR\\Common\\Crypto\\CryptoGen",
21+
"OpenEMR\\Common\\Database\\QueryUtils",
22+
"OpenEMR\\Events\\Globals\\GlobalsInitializedEvent",
23+
"OpenEMR\\Services\\Globals\\GlobalSetting"
1824
],
1925
"php-core-extensions": [
2026
"Core",

composer.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
"require": {
2727
"php": ">=8.2",
2828
"ext-filter": "*",
29+
"symfony/event-dispatcher": "^6.4 || ^7.0",
2930
"symfony/http-foundation": "^6.4 || ^7.0",
3031
"symfony/yaml": "^6.4 || ^7.0"
3132
},
3233
"require-dev": {
3334
"ergebnis/composer-normalize": "^2.44",
35+
"openemr/openemr": ">=8.0.0 || 8.1.0.x-dev || dev-master",
3436
"phpstan/phpstan": "^2.1",
3537
"phpunit/phpunit": "^11.0",
3638
"rector/rector": "^2.0",
@@ -39,6 +41,59 @@
3941
"suggest": {
4042
"google/cloud-secret-manager": "Required for gcp-secret-manager provider in _secrets block (^2.1)"
4143
},
44+
"repositories": [
45+
{
46+
"type": "package",
47+
"package": {
48+
"name": "openemr/openemr",
49+
"version": "8.0.0",
50+
"autoload": {
51+
"psr-4": {
52+
"OpenEMR\\": "src/"
53+
}
54+
},
55+
"source": {
56+
"type": "git",
57+
"url": "https://github.com/openemr/openemr.git",
58+
"reference": "v8_0_0"
59+
}
60+
}
61+
},
62+
{
63+
"type": "package",
64+
"package": {
65+
"name": "openemr/openemr",
66+
"version": "8.1.0.x-dev",
67+
"autoload": {
68+
"psr-4": {
69+
"OpenEMR\\": "src/"
70+
}
71+
},
72+
"source": {
73+
"type": "git",
74+
"url": "https://github.com/openemr/openemr.git",
75+
"reference": "rel-810"
76+
}
77+
}
78+
},
79+
{
80+
"type": "package",
81+
"package": {
82+
"name": "openemr/openemr",
83+
"version": "dev-master",
84+
"autoload": {
85+
"psr-4": {
86+
"OpenEMR\\": "src/"
87+
}
88+
},
89+
"source": {
90+
"type": "git",
91+
"url": "https://github.com/openemr/openemr.git",
92+
"reference": "master"
93+
}
94+
}
95+
}
96+
],
4297
"minimum-stability": "stable",
4398
"prefer-stable": true,
4499
"autoload": {

src/ConfigService.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenCoreEMR\ModuleConfig;
6+
7+
use OpenEMR\Common\Crypto\CryptoGen;
8+
use OpenEMR\Common\Database\QueryUtils;
9+
10+
/**
11+
* Persist module settings to the OpenEMR globals table via upsert.
12+
*
13+
* @author Michael A. Smith <michael@opencoreemr.com>
14+
* @copyright Copyright (c) 2026 OpenCoreEMR Inc. <https://www.opencoreemr.com>
15+
*/
16+
class ConfigService
17+
{
18+
private const UPSERT_SQL = <<<'SQL'
19+
INSERT INTO `globals` (`gl_name`, `gl_index`, `gl_value`)
20+
VALUES (?, 0, ?)
21+
ON DUPLICATE KEY UPDATE `gl_value` = ?
22+
SQL;
23+
24+
/**
25+
* Save a setting to the globals table (plaintext).
26+
*/
27+
public function saveSetting(string $key, string $value): void
28+
{
29+
QueryUtils::sqlStatementThrowException(self::UPSERT_SQL, [$key, $value, $value]);
30+
}
31+
32+
/**
33+
* Encrypt a value with CryptoGen and save it to the globals table.
34+
*/
35+
public function saveEncryptedSetting(string $key, string $value): void
36+
{
37+
$encrypted = (new CryptoGen())->encryptStandard($value);
38+
$this->saveSetting($key, $encrypted);
39+
}
40+
}

src/GlobalsRegistrar.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenCoreEMR\ModuleConfig;
6+
7+
use OpenEMR\Events\Globals\GlobalsInitializedEvent;
8+
use OpenEMR\Services\Globals\GlobalSetting;
9+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
10+
use Symfony\Component\HttpFoundation\ParameterBag;
11+
12+
/**
13+
* Register a module's globals section: enable/disable toggle and settings page link.
14+
*
15+
* Eliminates the boilerplate every OCE module copy-pastes in its Bootstrap class.
16+
*
17+
* @author Michael A. Smith <michael@opencoreemr.com>
18+
* @copyright Copyright (c) 2026 OpenCoreEMR Inc. <https://www.opencoreemr.com>
19+
*/
20+
class GlobalsRegistrar
21+
{
22+
public function __construct(
23+
private readonly ParameterBag $globalsBag,
24+
) {
25+
}
26+
27+
/**
28+
* Subscribe to GlobalsInitializedEvent and register the module's globals section.
29+
*/
30+
public function register(
31+
EventDispatcherInterface $eventDispatcher,
32+
GlobalsSectionDescriptor $descriptor,
33+
): void {
34+
$globalsBag = $this->globalsBag;
35+
36+
$eventDispatcher->addListener(
37+
GlobalsInitializedEvent::EVENT_HANDLE,
38+
static function (GlobalsInitializedEvent $event) use ($descriptor, $globalsBag): void {
39+
$service = $event->getGlobalsService();
40+
41+
$service->createSection($descriptor->sectionName);
42+
43+
// Enable/disable toggle
44+
$enableSetting = new GlobalSetting(
45+
sprintf(xlt('Enable %s'), $descriptor->sectionName),
46+
GlobalSetting::DATA_TYPE_BOOL,
47+
'0',
48+
sprintf(xlt('Enable or disable the %s module'), $descriptor->sectionName),
49+
);
50+
$service->appendToSection(
51+
$descriptor->sectionName,
52+
$descriptor->enableKey,
53+
$enableSetting,
54+
);
55+
56+
// Settings page link
57+
$webroot = $globalsBag->getString('webroot');
58+
$settingsPath = $webroot
59+
. '/interface/modules/custom_modules/' . $descriptor->moduleDirName
60+
. '/public/settings.php';
61+
62+
$linkSetting = new GlobalSetting(
63+
xlt('Module Settings'),
64+
GlobalSetting::DATA_TYPE_HTML_DISPLAY_SECTION,
65+
'',
66+
xlt('Link to the module settings page'),
67+
);
68+
$linkSetting->addFieldOption(
69+
GlobalSetting::DATA_TYPE_OPTION_RENDER_CALLBACK,
70+
static function () use ($settingsPath, $descriptor): string {
71+
$url = attr($settingsPath);
72+
$label = xlt('Open Module Settings');
73+
$description = xlt($descriptor->settingsDescription);
74+
return <<<HTML
75+
<p>{$description}</p>
76+
<a href="{$url}" class="btn btn-secondary btn-sm"
77+
onclick="top.restoreSession()">{$label}</a>
78+
HTML;
79+
},
80+
);
81+
$service->appendToSection(
82+
$descriptor->sectionName,
83+
$descriptor->enableKey . '_settings_link',
84+
$linkSetting,
85+
);
86+
},
87+
);
88+
}
89+
}

src/GlobalsSectionDescriptor.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenCoreEMR\ModuleConfig;
6+
7+
/**
8+
* Parameters for registering a module's globals section (enable toggle + settings link).
9+
*
10+
* @author Michael A. Smith <michael@opencoreemr.com>
11+
* @copyright Copyright (c) 2026 OpenCoreEMR Inc. <https://www.opencoreemr.com>
12+
*/
13+
final readonly class GlobalsSectionDescriptor
14+
{
15+
/**
16+
* @param string $sectionName Display name for the globals section (e.g. "OpenCoreEMR Sinch Conversations")
17+
* @param string $moduleDirName Module directory name under custom_modules/ (e.g. "oce-module-sinch-conversations")
18+
* @param string $enableKey Globals key for the enable/disable toggle (e.g. "oce_sinch_conversations_enabled")
19+
* @param string $settingsDescription Help text shown above the settings link
20+
*/
21+
public function __construct(
22+
public string $sectionName,
23+
public string $moduleDirName,
24+
public string $enableKey,
25+
public string $settingsDescription,
26+
) {
27+
}
28+
}

tests/Mocks/MockCryptoGen.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenEMR\Common\Crypto;
6+
7+
/**
8+
* Mock CryptoGen — uses base64 to simulate encryption without real crypto dependencies.
9+
*/
10+
class CryptoGen
11+
{
12+
public function encryptStandard(string $value): string
13+
{
14+
return base64_encode($value);
15+
}
16+
}

tests/Mocks/MockQueryUtils.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenEMR\Common\Database;
6+
7+
/**
8+
* Mock QueryUtils to avoid database calls during tests.
9+
*/
10+
class QueryUtils
11+
{
12+
/** @var list<array{sql: string, binds: list<mixed>}> */
13+
private static array $queries = [];
14+
15+
private static ?\Throwable $nextException = null;
16+
17+
/**
18+
* @param list<mixed> $binds
19+
*/
20+
public static function sqlStatementThrowException(string $sql, array $binds = []): true
21+
{
22+
self::$queries[] = ['sql' => $sql, 'binds' => $binds];
23+
if (self::$nextException !== null) {
24+
$e = self::$nextException;
25+
self::$nextException = null;
26+
throw $e;
27+
}
28+
return true;
29+
}
30+
31+
public static function setNextException(\Throwable $e): void
32+
{
33+
self::$nextException = $e;
34+
}
35+
36+
/** @return list<array{sql: string, binds: list<mixed>}> */
37+
public static function getQueries(): array
38+
{
39+
return self::$queries;
40+
}
41+
42+
public static function reset(): void
43+
{
44+
self::$queries = [];
45+
self::$nextException = null;
46+
}
47+
}

tests/Mocks/openemr_functions.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
// Stubs for OpenEMR global functions used by library classes (xlt, attr, etc.).
6+
// Loaded by the test bootstrap before any test runs.
7+
8+
if (!function_exists('xlt')) {
9+
function xlt(string $text): string
10+
{
11+
return $text;
12+
}
13+
}
14+
15+
if (!function_exists('attr')) {
16+
function attr(string $text): string
17+
{
18+
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
19+
}
20+
}

0 commit comments

Comments
 (0)