diff --git a/src/Command/ImportCodesCommand.php b/src/Command/ImportCodesCommand.php index 9ee3797..af00024 100644 --- a/src/Command/ImportCodesCommand.php +++ b/src/Command/ImportCodesCommand.php @@ -13,6 +13,8 @@ namespace OpenCoreEMR\CLI\ImportCodes\Command; +use OpenCoreEMR\CLI\ImportCodes\Config\ConfigAccessorInterface; +use OpenCoreEMR\CLI\ImportCodes\Config\GlobalsAccessor; use OpenCoreEMR\CLI\ImportCodes\Service\CodeImporter; use OpenCoreEMR\CLI\ImportCodes\Service\OpenEMRConnector; use OpenCoreEMR\CLI\ImportCodes\Service\MetadataDetector; @@ -31,13 +33,20 @@ class ImportCodesCommand extends Command private ?OutputInterface $output = null; public function __construct( - private readonly CodeImporter $importer = new CodeImporter(), - private readonly OpenEMRConnector $connector = new OpenEMRConnector(), + ?ConfigAccessorInterface $config = null, + ?CodeImporter $importer = null, + ?OpenEMRConnector $connector = null, private readonly MetadataDetector $detector = new MetadataDetector() ) { + $config ??= new GlobalsAccessor(); + $this->importer = $importer ?? new CodeImporter($config); + $this->connector = $connector ?? new OpenEMRConnector($config); parent::__construct(); } + private readonly CodeImporter $importer; + private readonly OpenEMRConnector $connector; + protected function configure() { $supportedTypes = implode(', ', self::SUPPORTED_TYPES); diff --git a/src/Config/ConfigAccessorInterface.php b/src/Config/ConfigAccessorInterface.php new file mode 100644 index 0000000..3459c59 --- /dev/null +++ b/src/Config/ConfigAccessorInterface.php @@ -0,0 +1,42 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +namespace OpenCoreEMR\CLI\ImportCodes\Config; + +interface ConfigAccessorInterface +{ + /** + * Get a configuration value + */ + public function get(string $key, mixed $default = null): mixed; + + /** + * Check if a configuration key exists + */ + public function has(string $key): bool; + + /** + * Get a configuration value as a string + */ + public function getString(string $key, string $default = ''): string; + + /** + * Get a configuration value as a boolean + */ + public function getBoolean(string $key, bool $default = false): bool; + + /** + * Get a configuration value as an integer + */ + public function getInt(string $key, int $default = 0): int; +} diff --git a/src/Config/GlobalsAccessor.php b/src/Config/GlobalsAccessor.php new file mode 100644 index 0000000..23c2109 --- /dev/null +++ b/src/Config/GlobalsAccessor.php @@ -0,0 +1,79 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +namespace OpenCoreEMR\CLI\ImportCodes\Config; + +class GlobalsAccessor implements ConfigAccessorInterface +{ + /** + * @inheritDoc + */ + public function get(string $key, mixed $default = null): mixed + { + return $GLOBALS[$key] ?? $default; + } + + /** + * @inheritDoc + */ + public function has(string $key): bool + { + return isset($GLOBALS[$key]); + } + + /** + * @inheritDoc + */ + public function getString(string $key, string $default = ''): string + { + $value = $this->get($key, $default); + if (is_string($value)) { + return $value; + } + return is_scalar($value) || $value === null ? (string) $value : $default; + } + + /** + * @inheritDoc + */ + public function getBoolean(string $key, bool $default = false): bool + { + $value = $this->get($key, $default); + if (is_bool($value)) { + return $value; + } + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + /** + * @inheritDoc + */ + public function getInt(string $key, int $default = 0): int + { + $value = $this->get($key, $default); + if (is_int($value)) { + return $value; + } + return is_numeric($value) ? (int) $value : $default; + } + + /** + * Get all globals (use sparingly) + * + * @return array + */ + public function all(): array + { + return $GLOBALS; + } +} diff --git a/src/Service/CodeImporter.php b/src/Service/CodeImporter.php index 7562453..705a68b 100644 --- a/src/Service/CodeImporter.php +++ b/src/Service/CodeImporter.php @@ -13,6 +13,7 @@ namespace OpenCoreEMR\CLI\ImportCodes\Service; +use OpenCoreEMR\CLI\ImportCodes\Config\ConfigAccessorInterface; use OpenCoreEMR\CLI\ImportCodes\Exception\CodeImportException; use OpenCoreEMR\CLI\ImportCodes\Exception\FileSystemException; use OpenCoreEMR\CLI\ImportCodes\Exception\DatabaseLockException; @@ -24,6 +25,11 @@ class CodeImporter private int $lockRetryDelaySeconds = 30; private bool $waitedForLock = false; + public function __construct( + private readonly ConfigAccessorInterface $config + ) { + } + /** * Validate temporary directory is writable * @@ -283,11 +289,12 @@ public function cleanup(string $type): void */ public function getStagingFiles(string $type): array { - if (!isset($GLOBALS['temporary_files_dir']) || !is_string($GLOBALS['temporary_files_dir'])) { + $tempDir = $this->config->getString('temporary_files_dir'); + if ($tempDir === '') { return []; } - $stagingDir = $GLOBALS['temporary_files_dir'] . '/' . $type; + $stagingDir = $tempDir . '/' . $type; if (!is_dir($stagingDir)) { return []; } diff --git a/src/Service/OpenEMRConnector.php b/src/Service/OpenEMRConnector.php index 8ad4588..c37f4b8 100644 --- a/src/Service/OpenEMRConnector.php +++ b/src/Service/OpenEMRConnector.php @@ -13,6 +13,7 @@ namespace OpenCoreEMR\CLI\ImportCodes\Service; +use OpenCoreEMR\CLI\ImportCodes\Config\ConfigAccessorInterface; use OpenCoreEMR\CLI\ImportCodes\Exception\OpenEMRConnectorException; class OpenEMRConnector @@ -21,6 +22,11 @@ class OpenEMRConnector private string $site; private bool $initialized = false; + public function __construct( + private readonly ConfigAccessorInterface $config + ) { + } + /** * Initialize connection to OpenEMR */ @@ -52,7 +58,7 @@ public function initialize(string $openemrPath, string $site = 'default'): void require_once $standardTablesPath; // Verify database connection (using OpenEMR's own validation method) - if (!isset($GLOBALS['dbh']) || !$GLOBALS['dbh']) { + if (!$this->config->get('dbh')) { throw new OpenEMRConnectorException( "OpenEMR database connection failed - " . "check database configuration and ensure MySQL is running" @@ -60,7 +66,8 @@ public function initialize(string $openemrPath, string $site = 'default'): void } // Verify ADODB connection is working - if (!isset($GLOBALS['adodb']['db']) || !$GLOBALS['adodb']['db']) { + $adodb = $this->config->get('adodb'); + if (!is_array($adodb) || !isset($adodb['db']) || !$adodb['db']) { throw new OpenEMRConnectorException("OpenEMR ADODB database connection not established"); } @@ -111,7 +118,8 @@ public function getTempDir(): string throw new OpenEMRConnectorException("OpenEMR connector not initialized"); } - return $GLOBALS['temporary_files_dir'] ?? sys_get_temp_dir(); + $tempDir = $this->config->getString('temporary_files_dir'); + return $tempDir !== '' ? $tempDir : sys_get_temp_dir(); } /** diff --git a/tests/E2E/RealVocabularyImportE2ETest.php b/tests/E2E/RealVocabularyImportE2ETest.php index 2a0e6ff..283a3db 100644 --- a/tests/E2E/RealVocabularyImportE2ETest.php +++ b/tests/E2E/RealVocabularyImportE2ETest.php @@ -22,6 +22,7 @@ namespace OpenCoreEMR\CLI\ImportCodes\Tests\E2E; use OpenCoreEMR\CLI\ImportCodes\Command\ImportCodesCommand; +use OpenCoreEMR\CLI\ImportCodes\Config\GlobalsAccessor; use OpenCoreEMR\CLI\ImportCodes\Service\OpenEMRConnector; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; @@ -61,7 +62,7 @@ protected function setUp(): void $this->markTestSkipped('OpenEMR not available. Run inside Docker: task dev:start'); } - $this->connector = new OpenEMRConnector(); + $this->connector = new OpenEMRConnector(new GlobalsAccessor()); try { $this->connector->initialize(self::OPENEMR_PATH, self::SITE); } catch (\Throwable $e) { diff --git a/tests/E2E/VocabularyImportE2ETest.php b/tests/E2E/VocabularyImportE2ETest.php index 61a2635..bc81716 100644 --- a/tests/E2E/VocabularyImportE2ETest.php +++ b/tests/E2E/VocabularyImportE2ETest.php @@ -17,6 +17,7 @@ namespace OpenCoreEMR\CLI\ImportCodes\Tests\E2E; use OpenCoreEMR\CLI\ImportCodes\Command\ImportCodesCommand; +use OpenCoreEMR\CLI\ImportCodes\Config\GlobalsAccessor; use OpenCoreEMR\CLI\ImportCodes\Service\OpenEMRConnector; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RequiresPhpExtension; @@ -52,7 +53,7 @@ protected function setUp(): void ); } - $this->connector = new OpenEMRConnector(); + $this->connector = new OpenEMRConnector(new GlobalsAccessor()); try { $this->connector->initialize(self::OPENEMR_PATH, self::SITE); } catch (\Throwable $e) { @@ -268,7 +269,7 @@ public function cleanupRemovesTemporaryFiles(): void $this->assertEquals(0, $result); // Verify temp directory was cleaned up - $tempDir = $GLOBALS['temporary_files_dir'] . '/CQM_VALUESET'; + $tempDir = $this->connector->getTempDir() . '/CQM_VALUESET'; $this->assertDirectoryDoesNotExist($tempDir, 'Temp directory should be removed after cleanup'); } diff --git a/tests/Integration/ImportCodesIntegrationTest.php b/tests/Integration/ImportCodesIntegrationTest.php index 15cbc11..01fb174 100644 --- a/tests/Integration/ImportCodesIntegrationTest.php +++ b/tests/Integration/ImportCodesIntegrationTest.php @@ -17,6 +17,7 @@ namespace OpenCoreEMR\CLI\ImportCodes\Tests\Integration; use OpenCoreEMR\CLI\ImportCodes\Command\ImportCodesCommand; +use OpenCoreEMR\CLI\ImportCodes\Config\GlobalsAccessor; use OpenCoreEMR\CLI\ImportCodes\Service\CodeImporter; use OpenCoreEMR\CLI\ImportCodes\Service\MetadataDetector; use OpenCoreEMR\CLI\ImportCodes\Service\OpenEMRConnector; @@ -54,7 +55,7 @@ protected function setUp(): void } // Try to initialize the connector - $this->connector = new OpenEMRConnector(); + $this->connector = new OpenEMRConnector(new GlobalsAccessor()); try { $this->connector->initialize(self::OPENEMR_PATH, self::SITE); $this->openemrAvailable = true; @@ -92,7 +93,7 @@ public function openEmrConnectorCanGetTempDir(): void #[Test] public function codeImporterCanCheckIfVocabularyLoaded(): void { - $importer = new CodeImporter(); + $importer = new CodeImporter(new GlobalsAccessor()); // Should not throw exception when database is available $result = $importer->isVocabularyLoaded('RXNORM'); @@ -103,7 +104,7 @@ public function codeImporterCanCheckIfVocabularyLoaded(): void #[Test] public function codeImporterCanCheckIfAlreadyLoaded(): void { - $importer = new CodeImporter(); + $importer = new CodeImporter(new GlobalsAccessor()); // Check with fake data - should return false $result = $importer->isAlreadyLoaded( diff --git a/tests/Unit/Command/ImportCodesCommandTest.php b/tests/Unit/Command/ImportCodesCommandTest.php index 828d6b5..8f6733b 100644 --- a/tests/Unit/Command/ImportCodesCommandTest.php +++ b/tests/Unit/Command/ImportCodesCommandTest.php @@ -39,6 +39,7 @@ protected function setUp(): void $this->detectorMock = $this->createMock(MetadataDetector::class); $this->command = new ImportCodesCommand( + null, $this->importerMock, $this->connectorMock, $this->detectorMock diff --git a/tests/Unit/Config/GlobalsAccessorTest.php b/tests/Unit/Config/GlobalsAccessorTest.php new file mode 100644 index 0000000..19789f6 --- /dev/null +++ b/tests/Unit/Config/GlobalsAccessorTest.php @@ -0,0 +1,196 @@ + + * @copyright Copyright (c) 2026 OpenCoreEMR Inc + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +namespace OpenCoreEMR\CLI\ImportCodes\Tests\Unit\Config; + +use OpenCoreEMR\CLI\ImportCodes\Config\ConfigAccessorInterface; +use OpenCoreEMR\CLI\ImportCodes\Config\GlobalsAccessor; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +class GlobalsAccessorTest extends TestCase +{ + private GlobalsAccessor $accessor; + + protected function setUp(): void + { + $this->accessor = new GlobalsAccessor(); + // Clear test globals before each test + unset($GLOBALS['test_string'], $GLOBALS['test_int'], $GLOBALS['test_bool'], $GLOBALS['test_array']); + } + + protected function tearDown(): void + { + // Clean up globals after each test + unset($GLOBALS['test_string'], $GLOBALS['test_int'], $GLOBALS['test_bool'], $GLOBALS['test_array']); + } + + #[Test] + public function implementsConfigAccessorInterface(): void + { + $this->assertInstanceOf(ConfigAccessorInterface::class, $this->accessor); + } + + #[Test] + public function getReturnsValueWhenKeyExists(): void + { + $GLOBALS['test_string'] = 'hello'; + + $this->assertEquals('hello', $this->accessor->get('test_string')); + } + + #[Test] + public function getReturnsDefaultWhenKeyNotExists(): void + { + $this->assertEquals('default', $this->accessor->get('nonexistent', 'default')); + } + + #[Test] + public function getReturnsNullWhenKeyNotExistsAndNoDefault(): void + { + $this->assertNull($this->accessor->get('nonexistent')); + } + + #[Test] + public function hasReturnsTrueWhenKeyExists(): void + { + $GLOBALS['test_string'] = 'hello'; + + $this->assertTrue($this->accessor->has('test_string')); + } + + #[Test] + public function hasReturnsFalseWhenKeyNotExists(): void + { + $this->assertFalse($this->accessor->has('nonexistent')); + } + + #[Test] + public function getStringReturnsStringValue(): void + { + $GLOBALS['test_string'] = 'hello'; + + $this->assertEquals('hello', $this->accessor->getString('test_string')); + } + + #[Test] + public function getStringConvertsIntToString(): void + { + $GLOBALS['test_int'] = 42; + + $this->assertEquals('42', $this->accessor->getString('test_int')); + } + + #[Test] + public function getStringReturnsDefaultForNonScalar(): void + { + $GLOBALS['test_array'] = ['foo' => 'bar']; + + $this->assertEquals('default', $this->accessor->getString('test_array', 'default')); + } + + #[Test] + public function getStringReturnsDefaultWhenKeyNotExists(): void + { + $this->assertEquals('default', $this->accessor->getString('nonexistent', 'default')); + } + + #[Test] + public function getBooleanReturnsBoolValue(): void + { + $GLOBALS['test_bool'] = true; + + $this->assertTrue($this->accessor->getBoolean('test_bool')); + } + + #[Test] + public function getBooleanConvertsStringTrue(): void + { + $GLOBALS['test_string'] = 'true'; + + $this->assertTrue($this->accessor->getBoolean('test_string')); + } + + #[Test] + public function getBooleanConvertsStringFalse(): void + { + $GLOBALS['test_string'] = 'false'; + + $this->assertFalse($this->accessor->getBoolean('test_string')); + } + + #[Test] + public function getBooleanConvertsNumericOne(): void + { + $GLOBALS['test_int'] = 1; + + $this->assertTrue($this->accessor->getBoolean('test_int')); + } + + #[Test] + public function getBooleanConvertsNumericZero(): void + { + $GLOBALS['test_int'] = 0; + + $this->assertFalse($this->accessor->getBoolean('test_int')); + } + + #[Test] + public function getBooleanReturnsDefaultWhenKeyNotExists(): void + { + $this->assertTrue($this->accessor->getBoolean('nonexistent', true)); + $this->assertFalse($this->accessor->getBoolean('nonexistent', false)); + } + + #[Test] + public function getIntReturnsIntValue(): void + { + $GLOBALS['test_int'] = 42; + + $this->assertEquals(42, $this->accessor->getInt('test_int')); + } + + #[Test] + public function getIntConvertsNumericString(): void + { + $GLOBALS['test_string'] = '123'; + + $this->assertEquals(123, $this->accessor->getInt('test_string')); + } + + #[Test] + public function getIntReturnsDefaultForNonNumeric(): void + { + $GLOBALS['test_string'] = 'not a number'; + + $this->assertEquals(99, $this->accessor->getInt('test_string', 99)); + } + + #[Test] + public function getIntReturnsDefaultWhenKeyNotExists(): void + { + $this->assertEquals(99, $this->accessor->getInt('nonexistent', 99)); + } + + #[Test] + public function allReturnsGlobalsArray(): void + { + $GLOBALS['test_string'] = 'hello'; + + $all = $this->accessor->all(); + + $this->assertIsArray($all); + $this->assertArrayHasKey('test_string', $all); + $this->assertEquals('hello', $all['test_string']); + } +} diff --git a/tests/Unit/Service/CodeImporterTest.php b/tests/Unit/Service/CodeImporterTest.php index f35a3ef..9a29c03 100644 --- a/tests/Unit/Service/CodeImporterTest.php +++ b/tests/Unit/Service/CodeImporterTest.php @@ -13,6 +13,7 @@ namespace OpenCoreEMR\CLI\ImportCodes\Tests\Unit\Service; +use OpenCoreEMR\CLI\ImportCodes\Config\GlobalsAccessor; use OpenCoreEMR\CLI\ImportCodes\Exception\CodeImportException; use OpenCoreEMR\CLI\ImportCodes\Exception\FileSystemException; use OpenCoreEMR\CLI\ImportCodes\Service\CodeImporter; @@ -25,7 +26,7 @@ class CodeImporterTest extends TestCase protected function setUp(): void { - $this->importer = new CodeImporter(); + $this->importer = new CodeImporter(new GlobalsAccessor()); } #[Test] diff --git a/tests/Unit/Service/OpenEMRConnectorTest.php b/tests/Unit/Service/OpenEMRConnectorTest.php index 5d01cf1..4fa2bde 100644 --- a/tests/Unit/Service/OpenEMRConnectorTest.php +++ b/tests/Unit/Service/OpenEMRConnectorTest.php @@ -13,6 +13,7 @@ namespace OpenCoreEMR\CLI\ImportCodes\Tests\Unit\Service; +use OpenCoreEMR\CLI\ImportCodes\Config\GlobalsAccessor; use OpenCoreEMR\CLI\ImportCodes\Exception\OpenEMRConnectorException; use OpenCoreEMR\CLI\ImportCodes\Service\OpenEMRConnector; use PHPUnit\Framework\Attributes\Test; @@ -24,7 +25,7 @@ class OpenEMRConnectorTest extends TestCase protected function setUp(): void { - $this->connector = new OpenEMRConnector(); + $this->connector = new OpenEMRConnector(new GlobalsAccessor()); } #[Test]