.phpstan/README.md
This directory contains custom PHPStan rules to enforce modern coding patterns in OpenEMR.
Purpose: Prevents direct $GLOBALS array access in favor of OEGlobalsBag::getInstance().
What it catches:
$GLOBALS['key'] - Direct array access (single or double quotes)$GLOBALS["key"] - Direct array access with double quotes$value = $GLOBALS['setting'] - Variable assignment from $GLOBALSfunction($GLOBALS['param']) - Passing $GLOBALS values as parametersWhat it doesn't catch (intentionally):
global $GLOBALS; - Global declarations (rare edge case, can be addressed separately if needed)$GLOBALS in comments or stringsRationale:
OEGlobalsBag can be mocked in unit tests$GLOBALS superglobal dependencyBefore (❌ Forbidden):
$value = $GLOBALS['some_setting'];
After (✅ Recommended):
use OpenEMR\Core\OEGlobalsBag;
$globals = OEGlobalsBag::getInstance();
$value = $globals->get('some_setting');
// Note: For encrypted values, you still need CryptoGen:
use OpenEMR\Common\Crypto\CryptoGen;
$cryptoGen = new CryptoGen();
$apiKey = $cryptoGen->decryptStandard($globals->get('gateway_api_key'));
Purpose: Prevents use of legacy functions:
sql.inc.php functions in the src/ directorycall_user_func() and call_user_func_array() functions (use modern PHP syntax instead)error_log() function (use Psr\Log\LoggerInterface instead, through OpenEMR\BC\ServiceContainer::getLogger() if needed)Rationale for SQL functions: Contributors should use QueryUtils or DatabaseQueryTrait instead for modern database patterns.
Rationale for call_user_func:
...) provides cleaner syntax...$args are more readable than array-based argumentsBefore (❌ Forbidden):
// Legacy dynamic function calls
$result = call_user_func('myFunction', $arg1, $arg2);
$result = call_user_func_array('myFunction', [$arg1, $arg2]);
$result = call_user_func([$object, 'method'], $arg1);
$result = call_user_func_array([$object, 'method'], $args);
After (✅ Recommended):
// Modern PHP 7+ syntax
$result = myFunction($arg1, $arg2);
// Dynamic function name
$functionName = 'myFunction';
$result = $functionName($arg1, $arg2);
// With argument unpacking
$args = [$arg1, $arg2];
$result = $functionName(...$args);
// Object method calls
$result = $object->method($arg1);
// or with callable syntax
$callable = [$object, 'method'];
$result = $callable($arg1, $arg2);
// or with argument unpacking
$result = $callable(...$args);
Rationale for error_log:
LoggerInterface can be mocked in unit testsBefore (❌ Forbidden):
error_log("Something went wrong: " . $error);
error_log("User {$userId} logged in");
After (✅ Recommended):
use OpenEMR\BC\ServiceContainer();
$logger = ServiceContainer::getLogger();
$logger->error("Something went wrong", ['error' => $error]);
$logger->info("User logged in", ['userId' => $userId]);
Purpose: Prevents use of laminas-db classes outside of the zend_modules directory.
Rationale: Laminas-DB is deprecated and scheduled for removal.
Purpose: Prevents use of @covers annotations in test method docblocks.
Rationale: The @covers annotation in PHPUnit tests causes any code that is used transitively or ancillary to the annotated code to be excluded from coverage reports. This results in incomplete coverage information and makes it harder to understand which code paths are actually being exercised by our test suite.
Before (❌ Forbidden):
/**
* @covers \OpenEMR\Services\SomeService
*/
public function testSomeMethod(): void
{
// test code
}
After (✅ Recommended):
public function testSomeMethod(): void
{
// test code - coverage is tracked automatically for all exercised code
}
Purpose: Prevents use of @covers annotations in test class docblocks.
Rationale: Same as NoCoversAnnotationRule - class-level @covers annotations also exclude transitively used code from coverage reports.
Before (❌ Forbidden):
/**
* @covers \OpenEMR\Services\SomeService
*/
class SomeServiceTest extends TestCase
{
// tests
}
After (✅ Recommended):
class SomeServiceTest extends TestCase
{
// tests - coverage is tracked automatically for all exercised code
}
Purpose: Prevents use of the empty() language construct.
What it catches:
if (empty($var)) - Empty check on any variableempty($array['key']) - Empty check on array access!empty($value) - Negated empty checksRationale:
empty("0") returns true (string "0" is considered empty)empty() on undefined variables returns true without warningempty(0), empty(0.0), empty([]), empty(null), empty(false), and empty("") all return trueBefore (❌ Forbidden):
if (empty($value)) {
// What are we actually checking for?
}
After (✅ Recommended):
// Be explicit about what you're checking
if ($value === null) { // Check for null
if ($value === '') { // Check for empty string
if ($value === null || $value === '') { // Check for null or empty string
if (count($array) === 0) { // Check for empty array
if (!$array) { // Boolean check on array (empty array is falsy)
// For checking if a variable or key exists
if (isset($var)) { // Check if variable is set and not null
if (isset($array['key'])) { // Check if array key exists and is not null
if (array_key_exists('key', $array)) { // Check if array key exists (even if null)
Configuration: This rule is provided by phpstan/phpstan-strict-rules with only disallowedEmpty enabled:
parameters:
strictRules:
allRules: false
disallowedEmpty: true
Purpose: Prevents use of raw curl_* functions throughout the codebase.
What it catches:
curl_init() - Initialize a cURL sessioncurl_setopt() - Set an option for a cURL transfercurl_exec() - Execute a cURL sessioncurl_close() - Close a cURL sessioncurl_* function callsRationale:
Before (❌ Forbidden):
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer token']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
// handle error
}
$data = json_decode($response, true);
After (✅ Recommended):
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
try {
$client = new Client();
$response = $client->request('GET', 'https://api.example.com/data', [
'headers' => [
'Authorization' => 'Bearer token'
]
]);
$data = json_decode($response->getBody()->getContents(), true);
} catch (GuzzleException $e) {
// handle error with proper exception
$this->logger->error('API request failed', ['exception' => $e]);
}
Or using OpenEMR's oeHttp wrapper:
use OpenEMR\Common\Http\oeHttp;
$response = oeHttp::get('https://api.example.com/data', [
'headers' => [
'Authorization' => 'Bearer token'
]
]);
$data = json_decode($response->getBody()->getContents(), true);
Existing violations are recorded in .phpstan/baseline/ as individual PHP files, organized by error type. The loader.php file includes all baseline files. New code should follow the patterns documented above.
To regenerate the baseline after fixing violations:
composer phpstan-baseline
composer phpstan