phpcycles finds places where the modules of your PHP project depend on each other in a circle, and shows you the file and line where each circle closes.
$ vendor/bin/phpcycles
4 modules, 4 files from ./composer.json (psr-4 autoload)
✗ 1 cycle in 1 tangle (3 of 4 modules)
Cut first
Orders -> Shipping on 1 of 1 cycle · 1 reference in 1 file
src/Orders/Order.php:8 Parcel
Shipping -> Billing on 1 of 1 cycle · 1 reference in 1 file
src/Shipping/Parcel.php:8 Invoice
Billing -> Orders on 1 of 1 cycle · 2 references in 1 file
src/Billing/Invoice.php:8 Order
Shortest cycles
1. Billing -> Orders -> Shipping -> Billing
Billing -> Orders src/Billing/Invoice.php:8
Orders -> Shipping src/Orders/Order.php:8
Shipping -> Billing src/Shipping/Parcel.php:8
Every reference: phpcycles deps Orders --why (with the same -c/-d/-x options)
Here Billing uses Orders, Orders uses Shipping, and Shipping uses Billing again. Modules caught in such a loop form a tangle. To break it, you only have to remove one of these dependencies. "Cut first" suggests which one: the one that breaks the most cycles and touches the fewest files.
composer require --dev heller/phpcyclesvendor/bin/phpcycles # checks the project and fails if it finds a cycle
vendor/bin/phpcycles -x Tests/ # leaves out test code
vendor/bin/phpcycles -f json # JSON output, or -f github for GitHub annotations
vendor/bin/phpcycles deps Billing --why # everything Billing uses, with file and linevendor/bin/phpcycles --help shows all commands and options.
Without any config, phpcycles takes the modules from composer.json: every folder directly below a psr-4 path becomes a module, like app/Http or app/Models. If you want to decide yourself, add a phpcycles.php next to composer.json. Paths in it are relative to that file.
<?php
return [
'exclude' => ['Tests/', 'Fixtures/'],
'detectors' => [
['namespace' => 'App', 'path' => 'src'], // src/Billing -> App\Billing, ...
],
'modules' => [
['name' => 'Kernel', 'namespace' => 'App\Kernel', 'path' => 'src/Kernel.php'],
],
];Already have cycles you can't fix right now? Record them once, and from then on only new ones make the check fail.
vendor/bin/phpcycles --generate-baseline phpcycles-baseline.txt
vendor/bin/phpcycles --baseline phpcycles-baseline.txtA module is a namespace. phpcycles reads every PHP file in the module folders, and each file belongs to the module whose namespace it declares, even if it sits in another module's folder.
Every class, function or constant your code uses counts, for example in new, extends or a type hint. A use statement alone doesn't count, only the code that actually uses the imported name. Comments, docblocks and strings are ignored, and so are files in vendor/ or listed in .gitignore. Names that only exist at runtime, like new $class, can't be seen.
phpcycles was inspired by Koriit/PHPDeps and can read its phpdeps.xml. If you want to enforce rules between layers, like "Domain must not use Http", have a look at deptrac or mago guard. phpcycles needs no rules, it only looks for cycles.
MIT