opencoreemr / openemr-phpstan-rules
PHPStan rules for OpenEMR core and module development
Installs: 48
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 1
Type:phpstan-extension
pkg:composer/opencoreemr/openemr-phpstan-rules
Requires
- php: ^8.2
- nikic/php-parser: ^5.0
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- spaze/phpstan-disallowed-calls: ^4.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.42
- phpunit/phpunit: ^11.0
- rector/rector: ^2.0
- squizlabs/php_codesniffer: ^4.0
This package is auto-updated.
Last update: 2026-01-18 00:08:14 UTC
README
Composer-installable PHPStan rules for OpenEMR core and module development. Enforces modern coding patterns and best practices.
Installation
composer require --dev opencoreemr/openemr-phpstan-rules
The rules are automatically loaded via phpstan/extension-installer. No manual configuration needed.
Important: Do not manually include extension.neon in your phpstan configuration. The extension-installer handles this automatically. Adding a manual include will cause "File included multiple times" warnings.
Bundled Extensions
This package includes and configures these PHPStan extensions:
- spaze/phpstan-disallowed-calls - Forbids legacy function calls
- phpstan/phpstan-deprecation-rules - Reports usage of deprecated code
Rules
Why Custom Rules Instead of Just @deprecated?
This package provides custom rules that forbid specific functions by name (e.g., sqlQuery(), call_user_func()). You might wonder why we don't just mark these functions as @deprecated in OpenEMR and rely on phpstan-deprecation-rules.
The reason: module analysis without OpenEMR loaded.
When running PHPStan on a standalone OpenEMR module, OpenEMR core may not be installed as a dependency or autoloaded. PHPStan's deprecation rules require the actual function/class definitions to read @deprecated annotations. If OpenEMR isn't available at scan-time, those annotations can't be read.
Our custom rules work by function name matching, so they catch forbidden calls even when the function definitions aren't available. This ensures modules get the same static analysis protection whether they're analyzed standalone or within a full OpenEMR installation.
Database Rules
Disallowed SQL Functions (via spaze/phpstan-disallowed-calls)
- Forbids: Legacy
sql.inc.phpfunctions (sqlQuery,sqlStatement,sqlInsert, etc.) - Requires:
QueryUtilsmethods instead - Example:
// ❌ Forbidden $result = sqlStatement($sql, $binds); // ✅ Required $records = QueryUtils::fetchRecords($sql, $binds);
ForbiddenClassesRule
- Forbids: Laminas-DB classes (
Laminas\Db\Adapter,Laminas\Db\Sql, etc.) - Requires:
QueryUtilsorDatabaseQueryTrait
Globals Rules
ForbiddenGlobalsAccessRule
- Forbids: Direct
$GLOBALSarray access - Requires:
OEGlobalsBag::getInstance() - Example:
// ❌ Forbidden $value = $GLOBALS['some_setting']; // ✅ Required $globals = OEGlobalsBag::getInstance(); $value = $globals->get('some_setting');
Testing Rules
NoCoversAnnotationRule
- Forbids:
@coversannotations on test methods - Rationale: Excludes transitively used code from coverage reports
NoCoversAnnotationOnClassRule
- Forbids:
@coversannotations on test classes - Rationale: Same as above - incomplete coverage tracking
HTTP Rules
ForbiddenCurlFunctionsRule
- Forbids: Raw
curl_*functions (curl_init,curl_exec,curl_setopt, etc.) - Requires: PSR-18 HTTP client
- Example:
// ❌ Forbidden $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); // ✅ Required - use a PSR-18 HTTP client $response = $httpClient->sendRequest($request);
Legacy PHP Rules
Disallowed call_user_func (via spaze/phpstan-disallowed-calls)
- Forbids:
call_user_func()andcall_user_func_array() - Requires: First-class callables (PHP 8.1+)
- Example:
// ❌ Forbidden call_user_func([$object, 'method'], $arg1, $arg2); call_user_func_array('someFunction', $args); // ✅ Required - first-class callable syntax $callable = $object->method(...); $callable($arg1, $arg2); $callable = someFunction(...); $callable(...$args); // Static methods $callable = SomeClass::staticMethod(...); $callable($arg);
Exception Handling Rules
CatchThrowableNotExceptionRule
- Forbids:
catch (\Exception $e) - Requires:
catch (\Throwable $e) - Rationale: Catches both exceptions and errors (
TypeError,ParseError, etc.) - Example:
// ❌ Forbidden try { $service->doSomething(); } catch (\Exception $e) { // Misses TypeError, ParseError, etc. } // ✅ Required try { $service->doSomething(); } catch (\Throwable $e) { // Catches everything }
Controller Rules
NoSuperGlobalsInControllersRule
- Forbids:
$_GET,$_POST,$_FILES,$_SERVERin Controller classes - Requires: Symfony
Requestobject methods - Example:
// ❌ Forbidden in controllers $name = $_POST['name']; $filter = $_GET['filter']; // ✅ Required $request = Request::createFromGlobals(); $name = $request->request->get('name'); $filter = $request->query->get('filter');
NoLegacyResponseMethodsRule
- Forbids:
header(),http_response_code(),die(),exit, directechoin controllers - Requires: Symfony
Responseobjects - Example:
// ❌ Forbidden in controllers header('Location: /some/path'); http_response_code(404); echo json_encode($data); die('Error'); // ✅ Required return new RedirectResponse('/some/path'); return new Response($content, 404); return new JsonResponse($data); throw new ModuleException('Error');
ControllersMustReturnResponseRule
- Forbids: Controller methods returning
voidor no return type - Requires: Return type declaration of
Responseor subclass - Example:
// ❌ Forbidden public function handleRequest(): void { // ... } // ✅ Required public function handleRequest(): Response { return new Response($content); }
Baselines
If you're adding these rules to an existing codebase, generate a baseline to exclude existing violations:
vendor/bin/phpstan analyze --generate-baseline
New code will still be checked against all rules.
Development
Running Tests
composer install vendor/bin/phpunit
License
GNU General Public License v3.0 or later. See LICENSE
Authors
- Michael A. Smith michael@opencoreemr.com