heller / phpcycles
Finds circular dependencies between the modules of a PHP codebase and tells you which file and line closes each cycle
Requires
- php: ^8.2
- nikic/php-parser: ^5.6
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
- ext-dom: Reads a Koriit/PHPDeps phpdeps.xml
- ext-pcntl: Parses large codebases on several CPU cores (Linux, macOS)
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-21 06:18:47 UTC
README
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.
Install
composer require --dev heller/phpcycles
Usage
vendor/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 line
vendor/bin/phpcycles --help shows all commands and options.
Configuration
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'], ], ];
Baseline
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.txt
What counts as a dependency
A 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.
Related tools
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.
License
MIT