This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
The project requires PHP ^8.0 (composer.json); CI/dev pins 8.1 (.php-version.dist). PHP 8 features (constructor promotion, union types, trailing commas) are used freely — there is no PHP 7.x compatibility requirement.
PHPArkitect is a static-analysis CLI that enforces architectural rules over a PHP codebase. Users write rules as PHP in a phparkitect.php config file (e.g. "classes in App\Domain must not depend on App\Infrastructure") and run phparkitect check in CI. It parses source into an AST with nikic/php-parser, never executing user code.
It is a public open-source library (phparkitect/phparkitect on Packagist, MIT), installed as a dev dependency in thousands of projects and run mostly in CI.
The people who use this tool are developers writing rules and reading failures, so DX is a first-class concern, not an afterthought — weigh it in every change:
- Violation output is the product. When a rule fails, the message (class, broken rule, the
because(...)reason) must make the cause obvious. Keep it actionable and don't regress its clarity. - The config DSL is a public API. The fluent
Rule/Expression/ArchitectureAPI and expression class names are how users express intent — favour readable, discoverable, backwards-compatible naming. Breaking it breaks every consumer'sphparkitect.php. - Document user-facing changes in
README.mdanddocs/rules.mdin the same change; a new rule nobody can find isn't done. - Fail gracefully. Confusing errors, crashes on valid PHP, or noisy output are DX bugs — treat them as such.
Run a single test by name — the Makefile has pattern targets:
make test_SomeTest # → bin/phpunit --filter test_SomeTest
make SomeTest # → bin/phpunit --filter SomeTest
bin/phpunit --filter testMethodName
bin/phpunit tests/Unit/Analyzer/FileVisitorTest.phpBefore committing, make csfix && make psalm && make test must all pass (CI gates merges on a green build).
Analysis (src/Analyzer/) turns source into a queryable model. Most parser bugs are a missing handle* case in FileVisitor for some syntax — add the case and a fixture-based test.
Rules / Expressions (src/Rules/, src/Expression/) are the constraint model:
- An
Expression(src/Expression/Expression.php) is one composable boolean check over aClassDescription, producing aViolationwith a description when it fails. All concrete checks live insrc/Expression/ForClasses/. Every class here implementsExpression— that invariant is itself enforced byphparkitect.php. - A
Rulecombines a selector (that(...)), a constraint (should(...)), and abecause(...)reason. The fluent builder lives insrc/Rules/(Rule,RuleBuilder,ArchRule) with thethat → should → becausegrammar parsed insrc/Rules/DSL/. Thebecausestring is printed verbatim in violation output. src/RuleBuilders/Architecture/is a higher-level DSL on top of expressions: define namedComponents by namespace and declare allowed dependencies between them (Architecture::withComponents(...)->where(...)->shouldNotDependOnAnyComponent()).
src/PHPUnit/ exposes a helper so users can assert rules inside a PHPUnit suite instead of the CLI.
- Add a class in
src/Expression/ForClasses/implementingExpression. - Add a unit test in
tests/Unit/Expressions/covering pass, fail (assert the violation message), and edge cases. - Document it in
README.md("Available rules") anddocs/rules.md.
tests/ mirrors src/. E2E tests shell out to the built CLI/Phar; unit tests for the analyzer parse small fixture snippets.
- Code style is enforced by
.php-cs-fixer.dist.php— don't hand-format; runmake csfix. - The parser supports a configurable target PHP version (
--target-php-version,8.0–8.5); when touchingFileVisitor/analyzer code, consider syntax across that range. - This tool analyzes other people's code, so the analyzer must tolerate any valid PHP without crashing — prefer surfacing a
ParsingErrorover throwing. - Don't write runtime code (fallback defaults,
assert()calls, extra variables) whose only purpose is to satisfy Psalm. If a flagged case can't actually happen, use a targeted@psalm-suppress IssueNamewith a one-line comment explaining why, instead of adding behavior that only exists to change what the analyzer infers. If an issue is low-value across the whole codebase (e.g.ImplicitToStringCast), suppress it globally inpsalm.xmlrather than working around it at every call site.