Configuration
The shipped rules and limits are the starting point for every Festi project, not a law for each of them. A project with a reason to differ writes its own ruleset that takes the shipped one and changes one thing. Everything it does not mention still applies.
phpcs
Put phpcs.xml in the project root.
Changing a limit
<?xml version="1.0"?>
<ruleset name="Project">
<rule ref="Festi"/>
<rule ref="Generic.Metrics.NestingLevel">
<properties>
<property name="nestingLevel" value="5"/>
</properties>
</rule>
</ruleset>
| Sniff | Property | Default |
|---|---|---|
Generic.Files.LineLength |
lineLimit, absoluteLineLimit |
120 |
Generic.Metrics.CyclomaticComplexity |
complexity |
10 |
Generic.Metrics.NestingLevel |
nestingLevel |
3 |
SlevomatCodingStandard.Complexity.Cognitive |
warningThreshold, errorThreshold |
16 (reports above 15) |
Festi.NamingConventions.EntityIdNaming |
checkVariables |
true |
Festi.Commenting.PublicMethodDocBlock |
exemptPrefixes |
test |
Festi.Commenting.PublicMethodDocBlock |
exemptFilePattern |
files under test/ or tests/ |
Festi.NamingConventions.PrivateMethodUnderscore |
exemptMethods |
the PHPUnit lifecycle methods (setUp, tearDown, …) |
Festi.Functions.RoutingMethodTypeHint |
routingPrefixes, pluginClassSuffix |
the six routing prefixes, Plugin |
Festi.Architecture.DomainLayerDependency |
domainSegment |
Domain |
Festi.Architecture.DomainLayerDependency |
innerSegments |
Domain, Contracts |
Festi.Architecture.DomainLayerDependency |
outwardSegments |
Application, Infrastructure, Persistence, Adapter, Adapters |
Festi.Architecture.DomainLayerDependency |
serviceLocators |
Core::getInstance |
Switching a rule off
<rule ref="Festi">
<exclude name="Festi.Functions.RoutingMethodTypeHint"/>
</rule>
Or part of one. This keeps the method and constant id rules and drops the variable one:
<rule ref="Festi.NamingConventions.EntityIdNaming">
<properties>
<property name="checkVariables" value="false"/>
</properties>
</rule>
A list property is set with type="array". This lets a domain name its
context's Events namespace as well:
<rule ref="Festi.Architecture.DomainLayerDependency">
<properties>
<property name="innerSegments" type="array">
<element value="Domain"/>
<element value="Contracts"/>
<element value="Events"/>
</property>
</properties>
</rule>
To drop one part of the type rules, exclude its message code. This keeps native types required and stops asking what an array holds:
<rule ref="Festi">
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification"/>
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification"/>
<exclude name="SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingTraversableTypeHintSpecification"/>
</rule>
Turning an error into a warning
Every rule is an error. To have one printed without failing a run, give it the warning type by its message code:
<rule ref="Generic.Metrics.CyclomaticComplexity.TooHigh">
<type>warning</type>
</rule>
| Sniff | Message codes |
|---|---|
Generic.Metrics.CyclomaticComplexity |
TooHigh above 10, MaxExceeded above 20 |
Generic.Metrics.NestingLevel |
TooHigh above 3, MaxExceeded above 10 |
SlevomatCodingStandard.Complexity.Cognitive |
ComplexityTooHigh |
Leaving code out
<exclude-pattern>*/src/generated/*</exclude-pattern>
A pattern is matched against the absolute path. Write it so that it
cannot match a parent directory: */src/generated/*, not generated/*.
One line
if ($count === []) { // phpcs:ignore Festi.Arrays.EmptyArrayComparison -- $count may be 0
Name the rule and give the reason. A bare phpcs:ignore hides every rule
on the line.
PHPMD
Put phpmd.xml in the project root. PHPMD cannot re-declare a rule it
already took from a ruleset, so the rule is left out and taken again:
<?xml version="1.0"?>
<ruleset name="Project" xmlns="http://pmd.sf.net/ruleset/1.0.0">
<rule ref="vendor/festi-team/coding-standard/phpmd/ruleset.xml">
<exclude name="ParameterCount"/>
</rule>
<rule ref="vendor/festi-team/coding-standard/phpmd/ruleset.xml/ParameterCount">
<properties>
<property name="maximum" value="6"/>
</properties>
</rule>
</ruleset>
| Rule | Property | Default |
|---|---|---|
ExcessiveClassComplexity |
maximum |
50 |
CouplingBetweenObjects |
maximum |
13 |
CouplingBetweenObjects |
compositionRootSuffixes |
Plugin,Factory |
NPathComplexity |
minimum |
200 |
TooManyMethods |
maxmethods |
20 |
TooManyFields |
maxfields |
10 |
ParameterCount |
maximum |
4 |
ParameterCount |
constructorMaximum |
5 |
To leave code out, add <exclude-pattern>*/src/generated/*</exclude-pattern>.
For one class or method, PHPMD reads a docblock annotation:
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
Phan
The project's .phan/config.php merges the shared baseline and overrides
what it needs. Start from the template
(Installation).
Choosing plugins
The template's $festiPlugins list is the only place a plugin is enabled.
Drop a line to switch that rule off.
Tuning the security plugins
The defaults are the framework's own method names:
'plugin_config' => [
'sql_sink_method_names' => [
'getAll', 'getRow', 'getOne', 'getAssoc', 'getCol', 'query', 'select',
],
'sql_sanitizer_method_names' => [
'quote', 'quoteColumnName', 'quoteTableName', 'getSqlCondition', 'intval', 'floatval',
],
'security_guard_parameter_names' => ['isCheckPermission'],
],
Add a project's own query method to the sinks, its own escaping method to the sanitisers, and its own guard parameter names to the last list.
Target PHP version
The baseline analyses for PHP 8.3. A project that supports an older
version sets 'target_php_version' => '8.1', or syntax that is fatal there
passes.
Suppressing one issue
/** @suppress PhanUndeclaredMethod the method comes from a plugin loaded at run time */
Who picks a project ruleset up
| Runner | phpcs | PHPMD |
|---|---|---|
festi-quality |
uses .phpcs.xml, phpcs.xml or their .dist forms when present, and says so in the summary |
uses phpmd.xml or phpmd.xml.dist when present, and says so |
festi-phpcs-diff |
--standard=phpcs.xml |
|
| CI jobs | FESTI_PHPCS_STANDARD: "phpcs.xml" |
FESTI_PHPMD_RULESET: "phpmd.xml" |
phpcs run directly |
picks phpcs.xml up itself when no --standard is given |
pass the file as the ruleset argument |
Adopting a rule on a codebase with a backlog
Some rules report hundreds of existing violations on a mature project:
PublicMethodDocBlock, RoutingMethodTypeHint, the type rules and the
variable part of EntityIdNaming are the usual ones. Pick by how large the backlog is.
| Backlog | Do |
|---|---|
Fixable by phpcbf |
Fix it in one merge request that changes nothing else. |
| Small | Fix it by hand, the same way. |
| Large | Keep the rule on, gate on changed code only, and fix files as they are edited. |
| Not ready at all | Switch the rule off in phpcs.xml with a comment saying when it comes back. |
Gate on changed code. festi-phpcs-diff and festi-quality --diff
judge only what a branch changed, so a rule with a backlog still stops new
violations. The full-tree run stays as a report.
How a rule is narrowed to a change depends on what it judges:
| The rule judges | It counts for a change when |
|---|---|
| a line (most rules) | that line changed |
| a signature (a missing parameter or return type) | any line of the signature changed, so an old untyped method is not the debt of whoever edits its body |
a file (declare(strict_types=1)) |
the first line changed, which in practice means the file is new |
| a whole function (complexity) | a changed line is inside it and the figure is new or higher than at the base revision; in festi-phpcs-diff and festi-quality --diff alike |
| a whole class (size, coupling, parameter count) | the same, in festi-quality --diff |
For Phan, record a baseline. Enable the plugin, then record what is already there so only new issues fail:
vendor/bin/phan --config-file .phan/config.php --save-baseline .phan/baseline.php
Keep 'baseline_path' => __DIR__.'/baseline.php' in the project config,
and remove entries as the code behind them is fixed.