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.