PHP kódminőség fenntartás
Néha rámtör az "ezúttal mindent jól csinálok" érzés, ilyenkor megpróbálom az említett téma aspektusait a legmélyebb részletig kidolgozni. Két ilyen roham között általában van két verziónyi eltérés az eszközök között, így mindig van egy kis izgalom is, hogy éppen mi hogyan működik.
A felsorolt - többségében statikus kódelemző - szoftverek abban segítenek, hogy bizonyos közmegállapodás vagy egyénre/projektre szabott szabályok mentén megvizsgálja a gyártott PHP kódot és felhívja a figyelmet az eltérésekre. Mindegyik valahogy máshogy áll hozzá, máshogy kell konfigurálni, használni, más tartalmú és formájú lesz az eredmény. Egyesek jól támogatják a CI/CD integrációt, mások olyan kimenetet tudnak gyártani, amit aztán fejlettebb, aggregálási- és riportolási célú eszközökbe lehet betölteni.
Az én felhasználási módom nagyon egyszerű: egyszemélyes, egyedi kód, kis projekt, egy dev és egy éles rendszer, közötte pedig FTP kapcsolat ("Vér István EV" mód).
Részletezett eszközök:
PHPUnit
A PHP de-facto unit-tesztelő eszköze, aktív fejlesztés alatt, számomra elég full-fledged-nek tűnik a mai napig. Nagyon gyorsan tesztel (másodpercenként 100-200 assertion), informatív lefedettség-riportokat csinál HTML-ben.
Honlap: https://phpunit.de/index.html
Doksi: https://phpunit.de/getting-started/phpunit-11.html
Letöltés (PHAR): https://phar.phpunit.de/phpunit-11.phar
Használat:
php phpunit.phar --configuration phpunit.xml projektkonyvtar
A phpunit.xml tartalma (értelemszerűen testreszabandó, főleg a megadott könyvtárak):
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.3/phpunit.xsd"
backupGlobals="false"
backupStaticProperties="false"
cacheDirectory="../work/.phpunit.cache"
cacheResult="true"
colors="true"
columns="200"
requireCoverageMetadata="false"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnRisky="false"
stopOnSkipped="false"
stopOnWarning="false"
stopOnDefect="false"
failOnEmptyTestSuite="true"
failOnIncomplete="true"
failOnRisky="true"
failOnSkipped="false"
failOnWarning="true"
beStrictAboutChangesToGlobalState="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutCoverageMetadata="true"
enforceTimeLimit="false"
defaultTimeLimit="0"
timeoutForSmallTests="1"
timeoutForMediumTests="10"
timeoutForLargeTests="60"
stderr="false"
reverseDefectList="false"
executionOrder="depends,defects"
resolveDependencies="true"
testdox="false"
displayDetailsOnIncompleteTests="true"
displayDetailsOnSkippedTests="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
shortenArraysForExportThreshold="10"
displayDetailsOnPhpunitDeprecations="true"
failOnPhpunitDeprecation="true"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source
ignoreIndirectDeprecations="false"
restrictNotices="false"
restrictWarnings="false"
>
<include>
<directory suffix=".php">../src</directory>
</include>
</source>
<coverage
includeUncoveredFiles="true"
pathCoverage="true"
ignoreDeprecatedCodeUnits="false"
disableCodeCoverageIgnore="false"
>
<report>
<html outputDirectory="../work/coverage" lowUpperBound="50" highLowerBound="90" />
</report>
</coverage>
</phpunit>
Fontos: a coverage riporthoz szükség van az xdebug extension bekapcsolására (php.ini-ben):
zend_extension=xdebug-3.3.2-8.2-vs16-nts-x86_64 xdebug.mode=coverage
Példariport:
PHP CodeSniffer (phpcs) és PHP Code Beautifier and Fixer (phpcbf)
A marketing szerint tiszta kód és konzisztencia elősegítő eszköz. A phpcs csak kiírja a szabálysértéseket, a phpcbf képes ezekből néhányat automatikusan javítani (pl. kódformázással kapcsolatosakat).
Honlap: https://github.com/PHPCSStandards/PHP_CodeSniffer/
Doksi: https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki
Letöltés (PHAR): https://github.com/PHPCSStandards/PHP_CodeSniffer/releases
Használat:
php phpcs.phar -s --standard=phpcs.xml -l projektkonyvtar
Van néhány előre összeállított szabálykészlet, amiket lehet használni, ezeket amúgy ki lehet iratni a phpcs -i paranccsal, amiről két dolgot kell tudni:
- nem listázza a "Generic" készletet
- még benne van a listában a "MySource" szabálykészlet, ami a következő verziótól deprecated lesz
A phpcs.xml tartalma (a könyvtárakat és a kihagyott elemeket érdemes elsősorban testreszabni, de akár a szabályokat is, törekedtem azért egy elég szigorú eredményt összerakni):
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Custom rules" xsi:noNamespaceSchemaLocation="phpcs.xsd">
<description>The coding standard MVC Framework</description>
<file>src</file>
<exclude-pattern>*/tests</exclude-pattern>
<arg name="basepath" value="../src"/>
<arg name="colors"/>
<arg name="parallel" value="16"/>
<rule ref="PEAR"></rule>
<rule ref="PSR1"></rule>
<rule ref="PSR2"></rule>
<rule ref="PSR12"></rule>
<rule ref="Squiz"></rule>
<rule ref="Zend"></rule>
<rule ref="Generic">
<exclude name="Generic.Arrays.DisallowShortArraySyntax.Found" />
<exclude name="Generic.Classes.OpeningBraceSameLine.BraceOnNewLine" />
<exclude name="Generic.Files.EndFileNoNewline.Found" />
<exclude name="Generic.Formatting.MultipleStatementAlignment.NotSame" />
<exclude name="Generic.Formatting.SpaceAfterNot.Incorrect" />
<exclude name="Generic.Functions.OpeningFunctionBraceKernighanRitchie.BraceOnNewLine" />
<exclude name="Generic.PHP.ClosingPHPTag" />
<exclude name="Generic.PHP.UpperCaseConstant.Found" />
<exclude name="Generic.WhiteSpace.DisallowSpaceIndent.SpacesUsed" />
<exclude name="PEAR.Commenting.ClassComment.MissingAuthorTag" />
<exclude name="PEAR.Commenting.ClassComment.MissingCategoryTag" />
<exclude name="PEAR.Commenting.ClassComment.MissingLicenseTag" />
<exclude name="PEAR.Commenting.ClassComment.MissingLicenseTag" />
<exclude name="PEAR.Commenting.ClassComment.MissingLinkTag" />
<exclude name="PEAR.Commenting.ClassComment.MissingPackageTag" />
<exclude name="PEAR.NamingConventions.ValidFunctionName.PrivateNoUnderscore" />
<exclude name="PEAR.NamingConventions.ValidVariableName.PrivateNoUnderscore" />
<exclude name="Squiz.Arrays.ArrayDeclaration.DoubleArrowNotAligned" />
<exclude name="Squiz.Arrays.ArrayDeclaration.SingleLineNotAllowed" />
<exclude name="Squiz.Classes.ClassFileName.NoMatch" />
<exclude name="Squiz.Commenting.ClosingDeclarationComment.Missing" />
<exclude name="Squiz.Commenting.FileComment.AuthorTagOrder" />
<exclude name="Squiz.Commenting.FileComment.CopyrightTagOrder" />
<exclude name="Squiz.Commenting.FileComment.IncorrectAuthor" />
<exclude name="Squiz.Commenting.FileComment.IncorrectCopyright" />
<exclude name="Squiz.Commenting.FileComment.PackageTagOrder" />
<exclude name="Squiz.Commenting.FileComment.SpacingAfterOpen" />
<exclude name="Squiz.Commenting.FileComment.SubpackageTagOrder" />
<exclude name="Squiz.Commenting.FunctionComment.InvalidReturn" />
<exclude name="Squiz.Commenting.LongConditionClosingComment.Missing" />
<exclude name="Squiz.ControlStructures.ElseIfDeclaration.NotAllowed" />
<exclude name="Squiz.ControlStructures.InlineIfDeclaration.NoBrackets" />
<exclude name="Squiz.Files.FileExtension.ClassFound" />
<exclude name="Squiz.Formatting.OperatorBracket.MissingBrackets" />
<exclude name="Squiz.Functions.GlobalFunction.Found" />
<exclude name="Squiz.PHP.DisallowBooleanStatement.Found" />
<exclude name="Squiz.PHP.DisallowComparisonAssignment.AssignedComparison" />
<exclude name="Squiz.PHP.DisallowInlineIf.Found" />
<exclude name="Squiz.Strings.ConcatenationSpacing.PaddingFound" />
<exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar" />
<exclude name="Squiz.WhiteSpace.FunctionClosingBraceSpace.SpacingBeforeClose" />
<exclude name="Squiz.WhiteSpace.FunctionSpacing.AfterLast" />
<exclude name="Squiz.WhiteSpace.FunctionSpacing.BeforeFirst" />
<exclude name="Squiz.WhiteSpace.MemberVarSpacing.FirstIncorrect" />
<exclude name="Zend.NamingConventions.ValidVariableName.PrivateNoUnderscore" />
<exclude name="Generic.Formatting.NoSpaceAfterCast.SpaceFound" />
<exclude name="Squiz.ControlStructures.InlineIfDeclaration.NotSingleLine" />
<exclude name="Squiz.WhiteSpace.OperatorSpacing.SpacingBefore" />
</rule>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="140"/>
<property name="absoluteLineLimit" value="140"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.FunctionSpacing">
<properties>
<property name="spacing" value="1"/>
</properties>
</rule>
</ruleset>
Példa kimenet:
PHP Mess Detector (régen: PHP Depend)
A phpmd azt tűzte ki célul, hogy felhívja a figyelmet azokra a kódrészletekre, amik nem követik a best practice-eket, fölöslegesen bonyolultak, esetleg nem használt részletek/változók vannak benne.
Honlap: https://phpmd.org/
Doksi: https://phpmd.org/documentation/index.html
Letöltés (PHAR): https://phpmd.org/static/latest/phpmd.phar
Használat:
php phpmd.phar projektkonyvtar text phpmd.xml
A phpmd.xml tartalma:
<?xml version="1.0"?>
<ruleset
name="The coding standard MVC Framework"
xmlns="https://phpmd.org/xml/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://phpmd.org/xml/ruleset/1.0.0 http://phpmd.org/xml/ruleset_xml_schema_1.0.0.xsd"
xsi:noNamespaceSchemaLocation="http://phpmd.org/xml/ruleset_xml_schema_1.0.0.xsd"
>
<description>The coding standard MVC Framework</description>
<rule ref="rulesets/codesize.xml">
<exclude name="TooManyPublicMethods" />
</rule>
<rule ref="rulesets/cleancode.xml">
<exclude name="ElseExpression" />
<exclude name="MissingImport" />
</rule>
<rule ref="rulesets/controversial.xml" />
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/unusedcode.xml" />
</ruleset>
Példakimenet:
PHPStan
A PHP jelenlegi statikus típus-támogatása hagy maga után kívánnivalót. A phpstan célja, hogy a következő szintre emelje ezt, hozzáadva olyan dolgokat, amik még nem a nyelv szerves részei (pl. generikus típusok) és magával hozza annak összes előnyét (karbantarthatóság, csapatmunka, jövőállóság, stb).
Honlap: https://phpstan.org/
Doksi: https://phpstan.org/user-guide/getting-started
Letöltés (PHAR): https://github.com/phpstan/phpstan/releases
Használat:
php phpstan.phar analyse -c phpstan.neon %%projektkonyvtar
A phpstan.neon tartalma:
parameters:
level: max
featureToggles:
bleedingEdge: true
skipCheckGenericClasses!: []
explicitMixedInUnknownGenericNew: true
explicitMixedForGlobalVariables: true
explicitMixedViaIsArray: true
arrayFilter: true
arrayUnpacking: true
nodeConnectingVisitorCompatibility: false
nodeConnectingVisitorRule: true
disableCheckMissingIterableValueType: true
strictUnnecessaryNullsafePropertyFetch: true
looseComparison: true
consistentConstructor: true
checkUnresolvableParameterTypes: true
readOnlyByPhpDoc: true
phpDocParserRequireWhitespaceBeforeDescription: true
runtimeReflectionRules: true
notAnalysedTrait: true
curlSetOptTypes: true
listType: true
missingMagicSerializationRule: true
nullContextForVoidReturningFunctions: true
unescapeStrings: true
duplicateStubs: true
invarianceComposition: true
alwaysTrueAlwaysReported: true
disableUnreachableBranchesRules: true
varTagType: true
closureDefaultParameterTypeRule: true
newRuleLevelHelper: true
instanceofType: true
paramOutVariance: true
allInvalidPhpDocs: true
strictStaticMethodTemplateTypeVariance: true
propertyVariance: true
genericPrototypeMessage: true
stricterFunctionMap: true
excludePaths:
- ../tests/*
Churn-php
A churn-php segít megállapítani, hogy mit érdemes refaktorálni. Ezt egy varázsképlettel számolja ki, ami figyelembe veszi a fájl változásának ütemét (git logból) illetve a komplexitását. Ezekből előállít egy számot, ami indikátora lehet annak, hogy mihez érdemes nyúlni. Összehasonlítva azzal, ha pl. a kód keveset változik vagy egyszerű, akkor fölösleges lenne a refaktoráláson gondolkodni.
Honlap: https://github.com/bmitch/churn-php
Doksi: https://github.com/bmitch/churn-php
Letöltés (PHAR): https://github.com/bmitch/churn-php/releases
Használat:
php churn.phar run --configuration=php-churn.yml projektkonyvtar
A php-churn.xml tartalma:
filesToShow: 100 minScoreToShow: null maxScoreThreshold: null parallelJobs: 16 commitsSince: '2000-01-01' fileExtensions: - php directoriesToScan: - projektkonyvtar vcs: git cachePath: .churn.cache
Példakimenet:
PhpDeprecationDetector
Ahogy a neve állítja: kiszúrja azokat a dolgokat, amik elavultak, kivezetésre kerültek vagy fognak kerülni, röviden: ami eltörik, ha az app újabb PHP-vel fut.
Honlap: https://wapmorgan.github.io/PhpDeprecationDetector/
Doksi: https://wapmorgan.github.io/PhpDeprecationDetector/
Letöltés (PHAR): https://github.com/wapmorgan/PhpDeprecationDetector/releases
Használat:
php phpdd-2.0.33.phar scan projektkonyvtar
Példakimenet:
Psalm - PHP Static Analysis Tool
A psalm-et a vimeo tartja karban, statikus kódelemzést csinál, ahogyan a többi említett eszköz is. Ez is figyelmet fordít a típusokra illetve "hibás kódot keres". A doksijában le van pontosan írva az összes elérhető szabály, a lenti konfiggal pedig a legszigorúbb ellenőrzést kaphatjuk.
Honlap: https://psalm.dev/
Doksi: https://psalm.dev/docs/
Letöltés (PHAR): https://github.com/vimeo/psalm/releases
Használat:
php psalm.phar --config=psalm.xml projektkonyvtar
A psalm.xml fájl tartalma:
<?xml version="1.0"?>
<psalm errorLevel="1" resolveFromConfigFile="true" useDocblockTypes="false" findUnusedBaselineEntry="false" findUnusedCode="false">
<projectFiles>
<directory name="../src" />
</projectFiles>
</psalm>
Példakimenet:
PHPLint
Általános linter eszköz PHP-hez.
Honlap: https://github.com/overtrue/phplint
Doksi: https://github.com/overtrue/phplint/blob/main/docs/usage/console.md
Letöltés (PHAR): https://github.com/overtrue/phplint/releases
Használat:
php D:\usr\prg\php\pear\phplint.phar -c qa/phplint.yml
A phplint.yml tartalma:
path: ./src jobs: 16 extensions: - php exclude: - tests warning: true memory-limit: -1 cache: ./work/phplint.cache
Példakimenet:
Phan - PHP Analyzer
A phan célja nem a helyesség ellenőrzése, hanem a hibák kimutatása. Rengeteg pluginje van, a típushibáktól a clean kód aspektusaiig elég sokmindent felölel. Van pl. olyan pluginje, ami azt ellenőrzi, hogy egy modernebb PHP verzióhoz készült kód hol törhet el, ha régebbi értelmezővel futtatjuk.
Honlap: https://github.com/phan/phan
Doksi: https://github.com/phan/phan/wiki
Letöltés (PHAR): https://github.com/phan/phan/releases
Fontos, hogy szükséges hozzá a php_ast extension a php-hez, amit innen lehet beszerezni: https://downloads.php.net/~windows/pecl/releases/ast/.
A php.ini-be pedig csak ennyi kell utána:
extension=ast
Használat:
php phan.phar -k phan.php -C -m verbose
A phan.php tartalma:
<?php
declare(strict_types=1);
use Phan\Issue;
return [
'target_php_version' => null,
'pretend_newer_core_functions_exist' => true,
'allow_missing_properties' => false,
'null_casts_as_any_type' => false,
'null_casts_as_array' => false,
'array_casts_as_null' => false,
'strict_method_checking' => true,
'strict_param_checking' => true,
'strict_property_checking' => true,
'strict_return_checking' => true,
'scalar_implicit_cast' => false,
'scalar_array_key_cast' => false,
'scalar_implicit_partial' => [],
'ignore_undeclared_variables_in_global_scope' => false,
'backward_compatibility_checks' => false,
'check_docblock_signature_return_type_match' => true,
'check_docblock_signature_param_type_match' => true,
'prefer_narrowed_phpdoc_param_type' => true,
'prefer_narrowed_phpdoc_return_type' => true,
'analyze_signature_compatibility' => true,
'allow_method_param_type_widening' => false,
'guess_unknown_parameter_type_using_default' => false,
'phpdoc_type_mapping' => [],
'dead_code_detection' => false,
'unused_variable_detection' => true,
'force_tracking_references' => false,
'warn_about_redundant_use_namespaced_class' => true,
'quick_mode' => false,
'simplify_ast' => true,
'enable_class_alias_support' => false,
'generic_types_enabled' => true,
'warn_about_undocumented_throw_statements' => true,
'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => true,
'exception_classes_with_optional_throws_phpdoc' => [],
'max_literal_string_type_length' => 1000,
'consistent_hashing_file_order' => false,
'globals_type_map' => [],
'minimum_severity' => Issue::SEVERITY_LOW,
'suppress_issue_types' => [],
'whitelist_issue_types' => [],
'file_list' => [],
'exclude_file_regex' => '#tests/.*#',
'enable_include_path_checks' => true,
'include_paths' => ['.'],
'warn_about_relative_include_statement' => true,
'exclude_file_list' => [],
'processes' => 1,
'directory_list' => ['src'],
'analyzed_file_extensions' => ['php'],
'exclude_analysis_directory_list' => ['vendor/'],
'skip_slow_php_options_warning' => true,
'autoload_internal_extension_signatures' => [],
'ignore_undeclared_functions_with_known_signatures' => false,
'plugin_config' => ['php_native_syntax_check_max_processes' => 16],
'plugins' => [
'AddNeverReturnTypePlugin',
'AlwaysReturnPlugin',
'AvoidableGetterPlugin',
'ConstantVariablePlugin',
'DemoPlugin',
'DeprecateAliasPlugin',
'DollarDollarPlugin',
'DuplicateArrayKeyPlugin',
'DuplicateConstantPlugin',
'DuplicateExpressionPlugin',
'EmptyMethodAndFunctionPlugin',
'EmptyStatementListPlugin',
'FFIAnalysisPlugin',
'HasPHPDocPlugin',
'InlineHTMLPlugin',
'InvalidVariableIssetPlugin',
'InvokePHPNativeSyntaxCheckPlugin',
'LoopVariableReusePlugin',
'MoreSpecificElementTypePlugin',
'NoAssertPlugin',
'NonBoolBranchPlugin',
'NonBoolInLogicalArithPlugin',
'NotFullyQualifiedUsagePlugin',
'NumericalComparisonPlugin',
'PhanSelfCheckPlugin',
'PHPDocInWrongCommentPlugin',
'PHPDocRedundantPlugin',
'PHPDocToRealTypesPlugin',
'PHPUnitAssertionPlugin',
'PHPUnitNotDeadCodePlugin',
'PossiblyStaticMethodPlugin',
'PreferNamespaceUsePlugin',
'PregRegexCheckerPlugin',
'PrintfCheckerPlugin',
'RedundantAssignmentPlugin',
'RemoveDebugStatementPlugin',
'SimplifyExpressionPlugin',
'SleepCheckerPlugin',
'StaticVariableMisusePlugin',
'StrictComparisonPlugin',
'StrictLiteralComparisonPlugin',
'SuspiciousParamOrderPlugin',
'UnknownClassElementAccessPlugin',
'UnknownElementTypePlugin',
'UnreachableCodePlugin',
'UnsafeCodePlugin',
'UnusedSuppressionPlugin',
'UseReturnValuePlugin',
'WhitespacePlugin',
],
];
Példakimenet:
PHP Copy/Paste Detector (PHPCPD)
Ahogy a neve mondja: megkeresi azokat a kódrészleteket, amik többszörözve vannak. A projekt elvileg már nem aktív.
Honlap: https://github.com/sebastianbergmann/phpcpd
Doksi: https://github.com/sebastianbergmann/phpcpd
Letöltés (PHAR): https://phar.phpunit.de/phpcpd.phar
Használat:
php phpcpd.phar --fuzzy projektkonyvtar
Példakimenet:
Bónusz
A fenti eszközök futtatása akár kézi, akár automatizált, igénybe vesz egy kis időt. Viszont, ha hasonló funkcionalitást szeretnénk, mint pl. a jest-nél a "watch mode" (az egyik JavaScript unit-testing eszköz), akkor igénybe vehetjük a "nodemon", vagyis a node monitor nevű toolt.
Telepítés:
npm install -g nodemon
Ezzel elérhetővé válik a nodemon parancs, amit változatos módon felparaméterezhetünk, mint például:
nodemon --watch src --watch tests --ext php --exec "php phpunit.phar --no-coverage --configuration qa/phpunit.xml tests || exit 0" nodemon --watch src --watch qa --ext php,xml --exec "php D:\usr\prg\php\pear\phpcs.phar -s --standard=qa/phpcs.xml src | head -n 30" nodemon --watch src --watch qa --ext php,xml --exec "php D:\usr\prg\php\pear\phpmd.phar src text qa/phpmd.xml || exit 0"
Ezzel a módszerrel a megadott könyvtárakban lévő szintén megadott kiterjesztésű fájlokat figyeli, és amint bármelyik változik, újraindítja az --exec kapcsolóban adott parancsot. Azaz, minden mentés után automatikusan lefuttatja azt az eszközt, ami alapján éppen kódot csiszolunk.