Skip to content

Configuration

phil/dev-tools

Layering

Every configuration has three layers:

  1. The shared configuration, in config/ of this package: the rules every PHIL and Tilttek package follows.
  2. The thin file at the root of a project, written by init: it calls the shared configuration with its own directory, and holds what only the project needs — a Symfony container for PHPStan, a rule of its own.
  3. phil-dev.php, in the root and in each package: the package's paths and its exceptions, nothing else.

The shared configurations read every phil-dev.php themselves, so adding a package to a monorepo adds a directory and touches no file of the root: several batches can work at once in their own worktrees without conflicts.

ToolThin fileHow it is layered
PHP-CS-Fixer.php-cs-fixer.dist.phpCalls config/php-cs-fixer.php, which returns the Config.
Rectorrector.phpCalls config/rector.php, which returns the RectorConfigBuilder.
Arkitectarkitect.phpCalls config/arkitect.php, which returns the closure Arkitect runs.
PHPStanphpstan.neonIncludes config/phpstan.neon, which includes the forbidden calls and the paths of the workspace.
Deptracnonephil-dev deptrac generates the configuration; the root may add its own files.
Infectionnonephil-dev infection generates the configuration: Infection has no includes.
PHPUnitphpunit.dist.xmlWritten whole by init, with globs for the packages of a monorepo.
PHPBenchphpbench.jsonIncludes config/phpbench.json.
Psalmnonephil-dev taint generates the configuration of the taint analysis.
TwigStannonephil-dev twigstan generates its configuration and its PHPStan configuration.
Twig-CS-FixernoneThe tool's standard, on the template directories.
Biomenonephil-dev js generates the configuration.
churn-phpnonephil-dev hotspots generates the configuration.
cspell, markdownlint, Valenonephil-dev lint-docs generates their configurations.
CaptainHookcaptainhook.jsonWritten whole by init.

PHPStan reads the workspace through a file generated under var/phil-dev/, because it keys its cache on the content of its configuration files and would otherwise miss a change in a phil-dev.php.

phil-dev.php

A PHP file that returns an array. Every section is optional; paths are relative to the file.

<?php

declare(strict_types=1);

return [
    'status' => 'active',
    'paths' => ['src', 'tests'],
    'phpstan' => [
        'includes' => ['extension.neon'],
        'excludePaths' => ['src/Generated/*'],
        'ignoreErrors' => [
            // why, in one sentence
            ['identifier' => 'argument.type', 'path' => 'src/Legacy.php'],
        ],
    ],
    'rector' => ['skip' => [SomeRector::class => ['src/Legacy.php']]],
    'cs' => ['exclude' => ['Generated']],
    'infection' => [
        'excludes' => ['Infrastructure'],
        'mutators' => ['IncrementInteger' => ['ignoreSourceCodeByRegex' => ['private const int MAX_.*']]],
    ],
    'deptrac' => ['allow' => ['^League\\\\Uri\\\\.*']],
    'arkitect' => ['namespaces' => ['Legacy'], 'rules' => static fn (): array => []],
    'docs' => ['public' => false, 'language' => 'en', 'words' => ['Example'], 'translations' => 'blocking'],
    'gates' => ['js' => false],
    'phpunit' => ['parallel' => true],
    'psalm' => ['excludes' => ['src/Generated'], 'suppress' => ['TaintedHtml' => ['src/RawRenderer.php']]],
    'fuzz' => ['targets' => ['markup' => 'tests/Fuzz/markup.php'], 'budget' => 60],
    'bench' => ['threshold' => 10, 'worker' => ['script' => 'tests/Bench/worker.php']],
    'bc' => ['tags' => 'v*'],
    'split' => ['repository' => 'git@github.com:parler-haut-interagir-librement/example.git'],
    'twig' => ['paths' => ['templates'], 'loader' => 'tests/twig-loader.php'],
    'js' => ['paths' => ['assets/controllers'], 'rules' => ['style' => ['noNonNullAssertion' => 'off']]],
    'unused' => ['ignore' => ['symfony/polyfill-php86']],
    'hotspots' => ['files' => 10, 'since' => 'One year ago'],
];
SectionKeyMeaning
statusactive, the default, or planned: a package being written; see Planned packages.
pathsThe directories and files the tools read; src and tests by default.
packagesRoot only: the globs of the members of a monorepo; packages/* by default.
phpstanincludesExtensions of the package that PHPStan loads, such as the PHPStan extension a library ships.
excludePathsPaths PHPStan leaves out.
ignoreErrorsThe package's exceptions, each with a reason; unmatched ones fail.
rectorskipRules, paths, or rules with the paths they skip.
csexcludePaths PHP-CS-Fixer leaves out, relative to each directory read.
rulesRoot only: rules added to or changed in the shared code style.
infectiondirectoriesThe source directories; those of autoload by default.
excludesPaths left out of mutation, relative to each source directory.
mutatorsThe named exceptions of the package, merged with the others.
testFrameworkOptionsRoot only: the PHPUnit options; --testsuite=Unit by default.
deptracallowThe dependencies the package is allowed besides the default; see Dependencies.
layers, rulesetLayers of the package's own, and rules replacing the defaults of a layer.
default, configsRoot only: the default replaced; extra Deptrac configurations.
arkitectnamespacesNamespaces admitted besides those of autoload.
rulesA callable returning extra Arkitect rules.
docspublicSet once the package is split to its public repository: the CHANGELOG may then use every section.
forbiddenRoot only: regular expressions of the terms no package names.
languageen, the default, or fr: the language cspell reads; Vale reads English only. The root's is its members' default.
wordsWords cspell accepts besides the shared list; the root's apply to every member.
markdownlintRoot only: markdownlint rules over the shared ones.
translationsblocking, the default, or advisory: whether the failures of check-translations stop the gate. A package's value wins over the root's; see Quality gates.
gatesaudit, unused, normalize, twig, jsRoot only: false turns that gate off.
phpunitparallelRoot only: false runs the tests with PHPUnit instead of ParaTest.
psalmexcludesPaths the taint analysis leaves out.
suppressAn issue type, such as TaintedHtml, with the paths where it is a reviewed exception.
fuzztargetsA name and the PHP file of each fuzz target.
budget, corpusThe seconds each target is fuzzed for, 60 by default; the corpus directory, tests/Fuzz/corpus by default.
benchthresholdRoot only: the slowdown bench:compare allows, in percent; 10 by default.
workerThe script, requests and warmup of the worker-mode benchmark.
bctagsRoot only: the glob of the tags bc compares with; every tag by default.
splitrepository, branch, prefixWhere a finished member is split to, its branch, and its directory in a target monorepo.
twigpaths, loaderThe template directories; a PHP file returning the Twig\Environment TwigStan compiles with.
jspaths, rulesThe script directories; Biome rules by group.
biomeRoot only: the Biome version downloaded.
unusedignorePackages composer-unused leaves out, used only through configuration.
hotspotsfiles, sinceRoot only: how many files churn-php shows, and from when it reads the history.

A monorepo

The root's phil-dev.php lists its own paths and the globs of its members. Two more pieces avoid every central list:

  • Autoloading: the root's composer.json loads the members' classes from their own composer.json with "autoload-dev": {"files": ["vendor/phil/dev-tools/resources/autoload-members.php"]}, instead of listing each namespace. The members' PSR-4 prefixes are added to Composer's ClassLoader of the root, so that the tools that ask it where a class lives (findFile(), getPrefixesPsr4()) find them too.
  • Narrowing: PHIL_DEV_PACKAGES=tilttek-quests,tilttek-quests-bundle composer gates runs every gate on those members only; the generated rules still know every package, so the result is the same as in a full run.

Planned packages

A package being written keeps 'status' => 'planned' in its phil-dev.php: its skeleton, a README that describes what it will do, and a smoke test, written by phil-dev new --planned (see The package skeleton). It is incubated with the other members: every gate checks it, as it checks an active package.

While the code of the active packages is what a run is about, the planned ones are left out:

PHIL_DEV_SKIP_PLANNED=1 composer gates
vendor/bin/phil-dev check-docs --skip-planned
  • PHIL_DEV_SKIP_PLANNED accepts 1, true, yes and on; any other value, or none, keeps the planned packages.
  • --skip-planned, which every phil-dev command takes, sets the variable for the tools the command starts.
  • Each phil-dev command that leaves them out first writes, on the error output, how many it skipped and which: Skipped 2 planned packages: phil/a, tilttek/b.
  • The configurations of PHP-CS-Fixer, Rector, PHPStan and Arkitect read the variable through Tooling, so the Composer scripts that call those tools directly leave the planned packages out too, without a word.
  • test and infection run with var/phil-dev/phpunit/phpunit.xml: the root's PHPUnit configuration, each relative path made absolute, each skipped package excluded from every suite and from the source.
  • A command still works on a planned package named as its argument, and the root is never skipped.
  • The variable combines with PHIL_DEV_PACKAGES: the members it names, less the planned ones.