PHP Composer and PSR Standards: Setting Up a Modern Project
Practice3 minInnovialabs editorial team
In short
A modern PHP project starts with composer.json: Composer installs dependencies and provides PSR-4 autoloading, while PHP-CS-Fixer and PHPStan enforce PSR-12 style and catch bugs before runtime.
What a modern PHP project is made of
The minimum set worth starting any project with:
- Composer — dependency manager and class autoloader.
- PSR-4 — the rule that maps a class name to a file path.
- PSR-12 — a shared coding style, so any developer can read the project without adjusting.
- PHP-CS-Fixer — automatically formats code to that style.
- PHPStan — static analysis: finds type errors and typos without running the code.
PSRs are recommendations from the PHP-FIG group, supported by most frameworks and libraries.
Step 1. composer.json
Start with composer init or write the file by hand:
{
"name": "acme/shop",
"type": "project",
"require": {
"php": ">=8.2"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"friendsofphp/php-cs-fixer": "^3.0"
},
"autoload": {
"psr-4": { "Acme\\Shop\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Acme\\Shop\\Tests\\": "tests/" }
},
"config": {
"sort-packages": true
}
}
Key points:
- require is what production needs; require-dev holds development tools.
- Declare the PHP version in
requireso Composer refuses incompatible packages. - Commit composer.lock for applications. It pins exact versions so everyone gets the same build.
- On the server, install with
composer install --no-dev --optimize-autoloader.
Step 2. PSR-4 autoloading
PSR-4 maps a namespace to a folder. With the config above, Acme\Shop\Order\Invoice must live in src/Order/Invoice.php:
<?php
declare(strict_types=1);
namespace Acme\Shop\Order;
final class Invoice
{
public function __construct(
private readonly int $amount,
) {
}
}
Include the autoloader once at the entry point:
require __DIR__ . '/../vendor/autoload.php';
After changing the autoload section, run composer dump-autoload. File and folder names must match the class name's case exactly: on a Linux server invoice.php and Invoice.php are different files.
Step 3. Code style: PSR-12 and PHP-CS-Fixer
PSR-12 covers indentation, brace placement, use ordering and type declarations. You do not need to memorize it — PHP-CS-Fixer does the work. Config in .php-cs-fixer.dist.php:
<?php
$finder = PhpCsFixer\Finder::create()
->in([__DIR__ . '/src', __DIR__ . '/tests']);
return (new PhpCsFixer\Config())
->setRules(['@PSR12' => true])
->setFinder($finder);
Commands:
vendor/bin/php-cs-fixer fix— fix the code;vendor/bin/php-cs-fixer fix --dry-run --diff— only report violations (for CI).
You can swap the @PSR12 rule set for the newer @PER-CS, which evolves PSR-12.
Step 4. Static analysis: PHPStan
PHPStan checks types, missing methods and wrong arguments. The phpstan.neon file:
parameters:
level: 6
paths:
- src
- tests
Run it with vendor/bin/phpstan analyse.
Rolling it out without pain:
- In a new project, start at a high strictness level.
- In legacy code, start low and raise it gradually, or record current errors in a baseline (
--generate-baseline) and block only new ones.
Step 5. Scripts and CI
Group the commands in the scripts section of composer.json:
"scripts": {
"cs": "php-cs-fixer fix --dry-run --diff",
"stan": "phpstan analyse",
"check": ["@cs", "@stan"]
}
Now composer check runs the same way on a laptop and in CI. A pipeline that fails on style violations and analysis errors is more reliable than any team agreement.
Common mistakes
- Not committing composer.lock — the server installs different package versions.
- Editing code in vendor/ — changes vanish on the next install.
- Namespace and path mismatch — the class is not found, but only in production.
- Turning analysis to maximum on legacy code — thousands of errors, and the tool gets switched off.