composer-tooling
Composer conventions for any PHP project: dependency management, version constraints, PSR-4 autoloading, scripts, platform requirements, and the composer.json vs composer.lock contract. Stack-agnostic
SKILL.mdplugins/php-foundation/skills/composer-tooling/SKILL.md
GitHubComposer Tooling (stack-agnostic)
Composer is the dependency manager and autoloader entry point for every modern PHP project. Read composer.json before doing anything else — it tells you the PHP version, the framework, and the available packages.
Project detection
cat composer.json # PHP version (require.php), framework, scripts
test -f composer.lock && echo "locked" # lockfile present → reproducible installs
Key fields to read:
require.php— minimum PHP version (drives which language features you may use).require— runtime dependencies; the framework marker lives here (laravel/framework,symfony/framework-bundle).require-dev— dev/test tooling (phpunit, pest, phpstan, pint, php-cs-fixer).autoload.psr-4— namespace → directory map.config.platform.php— pins the version the resolver targets (may differ from the runtime PHP).
composer.json vs composer.lock
composer.jsondeclares intent — version ranges you accept.composer.lockrecords the exact resolved versions. It is committed and is the source of truth forcomposer install.
| Command | Effect |
|---|---|
composer install |
Installs the exact versions from composer.lock. Use in CI/deploy. |
composer update <pkg> |
Re-resolves the named package, rewrites the lock. Use deliberately. |
composer require <pkg> |
Adds to composer.json + updates the lock for that package. |
composer require --dev <pkg> |
Same, into require-dev. |
Never hand-edit composer.lock. Never run a bare composer update (updates everything) as part of a focused feature change.
Version constraints
{
"require": {
"php": "^8.2", // >=8.2.0 <9.0.0 — recommended for libraries/apps
"guzzlehttp/guzzle": "^7.8" // caret: compatible up to the next major
}
}
- Caret
^7.8— allows>=7.8.0 <8.0.0. The default; respects SemVer. - Tilde
~7.8.0— allows>=7.8.0 <7.9.0. Tighter; use when you must pin to a minor. - Exact
7.8.3— avoid except to work around a specific broken release. - Avoid
*and unbounded>=— they break reproducibility.
Adding a dependency — checklist
- Confirm it is not already provided transitively (
composer why-not <pkg>/ check existingrequire). - Choose
require(runtime) vsrequire-dev(tools/tests) correctly. - Use a caret constraint unless the project convention says otherwise.
- Run
composer require <pkg>so the lockfile updates atomically. - Verify nothing else moved: review the
composer.lockdiff — only the new package and its deps should change.
PSR-4 autoloading
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
}
- Namespace prefix maps to a base directory; sub-namespaces map to sub-directories, class name = file name.
- After adding a new top-level namespace mapping, run
composer dump-autoload. - Keep test namespaces in
autoload-devso they are excluded from production autoload maps. - For deploy/CI, generate an optimized classmap:
composer dump-autoload --optimize(or--classmap-authoritative).
Scripts
{
"scripts": {
"test": "phpunit",
"stan": "phpstan analyse",
"cs": "php-cs-fixer fix"
}
}
Run via composer test, composer stan. Prefer existing scripts over invoking the binary directly — they encode the project's intended flags. Discover them with composer run-script --list.
Platform requirements
{
"config": {
"platform": {
"php": "8.2.0"
}
}
}
config.platform.php tells Composer which PHP version to resolve against, independent of the local CLI version — keep it aligned with the production runtime so the lockfile is deployable.