Usage
Two commands ship with the package, and the three underlying tools can always be run directly.
| Command | Use it to |
|---|---|
festi-quality |
see how a project, some paths or a change stand across every tool |
festi-phpcs-diff |
gate a merge request on the style of the lines it changed |
phpcs, phpmd, phan |
run one tool with its own options |
Run everything from the project root.
festi-quality
Runs phpcs with the Festi standard, PHPMD with the shipped ruleset and
Phan with the project's .phan/config.php, reads the open issues SonarQube
holds for the project, and prints one summary.
Choose what it looks at
vendor/bin/festi-quality # the whole project
vendor/bin/festi-quality src tests/Unit # these directories
vendor/bin/festi-quality src/Billing/Invoice.php # one file
vendor/bin/festi-quality --diff # the lines this branch changed
vendor/bin/festi-quality --diff src # the lines this branch changed under src
vendor/bin/festi-quality --base=origin/main # the lines changed since that revision
"Changed" means three things together: what the branch committed since its
merge base, what the working tree changed since HEAD, and every line of a
file git does not track yet. .gitignore is honoured, so vendor/ is not
taken for new work. The command is therefore as useful before a commit as
it is in a pipeline.
Read the summary
Festi quality summary
Scope: whole project
phpcs 35 issues 35 errors, 0 warnings, 20 auto-fixable
phpmd 1 issue
phan 0 issues
sonar 1 issue server analysis of 2026-10-02 at 7eb7db8e, NOT the revision checked out (705bbe1d)
phpcs by rule
18 Generic.Arrays.DisallowLongArraySyntax.Found
9 Festi.NamingConventions.EntityIdNaming.VariableIdNaming
...
The Scope: line says what the counts are counts of. Each tool then has
one line, and three outcomes are kept apart because only the first means
the code is clean:
| Line | Means |
|---|---|
0 issues |
the tool ran and found nothing |
SKIPPED |
the tool does not apply: no .phan/config.php, no sonar.projectKey, no SonarQube credentials, or a change with no PHP in it. The reason follows. |
FAILED |
the tool is not installed, crashed, or the server refused. The reason follows. |
Add --list to see every issue with its place:
phpcs issues
src/Billing/Invoice.php:42 Generic.Arrays.DisallowLongArraySyntax.Found Short array syntax must be used to define arrays
Options
| Option | Default | Meaning |
|---|---|---|
--diff |
off | Look only at what changed since the base revision. |
--base=<ref> |
the merge request base, then the default branch on origin |
Revision to diff against; implies --diff. A ref that does not exist is an error, never a fallback. |
--list |
off | After the counts, list every issue as file:line rule message. |
--strict |
off | Exit 1 when any tool reported an issue. |
--only=<list> |
all four | Tools to run: phpcs,phpmd,phan,sonar. |
--ignore=<list> |
vendor/* |
Comma-separated patterns to leave out. PHPMD also always leaves out tests/*. |
--format=<name> |
text |
json carries every rule of every tool, the scope, and with --list every issue. The text form shows ten rules per tool. |
An option takes its value attached: --base=origin/main, not
--base origin/main.
Exit codes
| Code | Means |
|---|---|
0 |
The summary is complete. Without --strict this says nothing about what it found. |
1 |
With --strict: at least one tool reported an issue, warnings included. |
2 |
A tool could not produce a report, or the run could not start. This outranks 1: a broken tool may be hiding issues. |
So the same command is a report by default and a gate when asked:
# Before a commit: everything, on what I just wrote.
vendor/bin/festi-quality --diff --strict --only=phpcs,phpmd,phan
Choose the gating tools with --only. SonarQube's count comes from the
server's last analysis, not from your working tree, so it rarely belongs in
a gate.
How each tool takes the scope
The tools take their input differently, so the scope is applied to every issue they report rather than trusted to each command line.
| Tool | Paths | A change (--diff) |
|---|---|---|
| phpcs | handed the paths | handed the changed files. A message counts on a changed line; a missing type when the signature changed; a complexity message when a changed line is inside the function and the figure went up. |
| PHPMD | handed the paths | handed the changed files. A measure counts when a changed line is inside the class or method and the figure went up. |
| Phan | analyses the whole program; issues outside the paths are dropped | the same; issues off the changed lines are dropped |
| SonarQube | asked for the paths as components of the project | asked for the changed files, and not narrowed to lines |
Three things follow that are worth knowing:
- Phan takes as long on one file as on the project. It has to read the
whole program to type one line of it. Use
--only=phpcs,phpmdfor a quick look. - phpcs and PHPMD run twice under
--diff, once on the change and once on the base versions of the same files. See Did the change make it worse? - SonarQube is read, not run. The summary always names the revision the
server analysed and says whether it is the one checked out. Under
--diffit counts every open issue of the changed files and says so.
Did the change make it worse?
A measure of a whole function or class lands on any change that touches
one. On its own that says the change was near the debt, not that it added
to it. So under --diff, phpcs and PHPMD are run a second time on the
changed files as they were at the base revision, and each measure is set
beside its earlier figure:
| At the base | On the change | Reported |
|---|---|---|
| under the limit, or the function did not exist | over the limit | yes, (new since origin/main) |
| 12 | 15 | yes, (was 12 at origin/main) |
| 15 | 15 or lower | no; counted in the tool's note |
phpcs 1 issue before the comparison: 2 errors, 0 warnings, 0 auto-fixable; 1 measure(s) no worse than at origin/main left out
phpcs issues
src/common/SourceTrait.php:120 Generic.Metrics.NestingLevel.TooHigh Function's nesting level (5) exceeds 3 (was 4 at origin/main)
With --strict this is a gate that stops a change from adding complexity
and lets a fix inside an old, complex function through. festi-phpcs-diff
applies the same comparison to the complexity rules.
What is compared: the three complexity sniffs and every PHPMD rule. The
base is the merge base of the base revision and HEAD, the same point the
changed lines are counted from. A function is recognised by its name, so a
renamed function is new. When the clone cannot reach the base revision,
nothing is left out and the note says not compared with the base.
A project's own rulesets
When the project root holds phpcs.xml (or .phpcs.xml, or their .dist
forms) or phpmd.xml (or phpmd.xml.dist), festi-quality uses it in
place of the shipped rules and says so in the summary. See
Configuration.
festi-phpcs-diff
The merge request gate for style. It runs phpcs over the files a branch touched and keeps only the messages about changed code, so a standard with a backlog can still block new violations.
vendor/bin/festi-phpcs-diff --ignore='vendor/**' ./
| Option | Default | Meaning |
|---|---|---|
--base=<ref> |
$CI_MERGE_REQUEST_DIFF_BASE_SHA, origin/$CI_DEFAULT_BRANCH, origin/HEAD, then main/master/develop |
Revision to diff against. A ref that does not exist is an error. |
--standard=<name> |
Festi |
Standard to apply, or the path of a project ruleset. |
--ignore=<list> |
none | Comma-separated phpcs ignore patterns. |
Exit codes: 0 clean, 1 errors in changed code, 2 the check could not
run. Warnings are printed and do not fail the run.
A complexity error is about a whole function, so it is judged the way
festi-quality --diff judges it: reported when the branch changed the
function and the figure is higher than at the base revision, with what it
was. When the clone cannot reach the base revision the error stays.
It differs from festi-quality --diff --strict --only=phpcs in one way: it
fails on errors only, so a rule a project turned into a warning is shown
and does not block.
--ignorepatterns are matched against the absolute path. A pattern that reads as project-relative silences the whole tree when a parent directory happens to match it. If every changed file is ignored, the run passes and says so on stderr:festi-phpcs-diff: nothing was checked — all 6 changed PHP file(s) matched --ignore=vendor/**,Festi/**.
On a clone that cannot see the base revision (GitLab clones shallow by
default), set GIT_DEPTH: 0 or pass --base.
The tools one by one
# Style, the whole tree.
vendor/bin/phpcs --standard=Festi -d memory_limit=1024M --extensions=php --ignore='vendor/**' ./
# Fix what phpcs can fix itself.
vendor/bin/phpcbf --standard=Festi --extensions=php --ignore='vendor/**' ./
# Class size and coupling.
vendor/bin/phpmd ./ text vendor/festi-team/coding-standard/phpmd/ruleset.xml \
--exclude 'vendor/*,tests/*'
# Static analysis.
vendor/bin/phan --config-file .phan/config.php
Add --allow-polyfill-parser to the Phan call on a machine without
ext-ast.