Configuration

Back to the README

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.