Composer-installable PHPStan rules for OpenEMR core and module development. Enforces modern coding patterns and best practices.
composer require --dev opencoreemr/openemr-phpstan-rulesInclude the core ruleset in your phpstan.neon:
includes:
- vendor/opencoreemr/openemr-phpstan-rules/core.neonInclude the module ruleset in your phpstan.neon:
includes:
- vendor/opencoreemr/openemr-phpstan-rules/module.neonForbiddenFunctionsRule
- 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
ForbiddenGlobalsAccessRule
- Forbids: Direct
$GLOBALSarray access - Requires:
OEGlobalsBag::getInstance() - Example:
// ❌ Forbidden $value = $GLOBALS['some_setting']; // ✅ Required $globals = OEGlobalsBag::getInstance(); $value = $globals->get('some_setting');
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
These additional rules enforce Symfony-inspired MVC patterns in OpenEMR modules.
- 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 }
- 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');
- 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');
- 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); }
You can selectively enable rules by creating your own configuration:
# Custom phpstan.neon
services:
# Just database rules
- class: OpenCoreEMR\PHPStan\Rules\Database\ForbiddenFunctionsRule
tags:
- phpstan.rules.rule
# Just module controller rules
- class: OpenCoreEMR\PHPStan\Rules\Module\NoSuperGlobalsInControllersRule
tags:
- phpstan.rules.ruleIf you're adding these rules to an existing codebase, generate a baseline to exclude existing violations:
vendor/bin/phpstan analyze --generate-baselineNew code will still be checked against all rules.
See MIGRATION_GUIDE.md for detailed migration patterns for each rule.
# Install dependencies
composer install
# Run PHPStan on the rules themselves
vendor/bin/phpstan analyzeContributions are welcome! Please:
- Follow existing code style and patterns
- Add tests for new rules
- Update documentation
GNU General Public License v3.0 or later. See LICENSE
- Michael A. Smith [email protected]