Modern PHP Engineering
Chapitre 4
04 — Composer et l'Écosystème PHP
04 — Composer et l'Écosystème PHP
Cours 04 — Composer et l'Écosystème PHP
04.1 Composer : fondamentaux
Composer est le gestionnaire de dépendances PHP créé par Nils Adermann et Jordi Boggiano. Inspiré de npm (Node.js) et Bundler (Ruby), il est devenu le standard incontesté.
Installation
# Installation globale
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
# Vérification
composer --version
# Composer version 2.8.x 2025-xx-xx
# Mise à jour
composer self-update
composer.json
{
"name": "vendor/project",
"description": "Projet moderne PHP",
"type": "project",
"require": {
"php": ">=8.2",
"laravel/framework": "^11.0",
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.0",
"friendsofphp/php-cs-fixer": "^3.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"cs-fix": "php-cs-fixer fix src/"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}
composer.lock
Le fichier composer.lock fige les versions exactes installées. Il doit être commité dans le dépôt.
composer install # Lit composer.lock
composer update # Met à jour vers les dernières versions compatibles
04.2 Commandes essentielles
# Initialiser un projet
composer init
# Ajouter une dépendance
composer require laravel/framework
composer require --dev phpunit/phpunit
# Supprimer une dépendance
composer remove phpunit/phpunit
composer remove --dev phpunit/phpunit
# Installer les dépendances
composer install
composer install --no-dev # Production
composer install --optimize-autoloader # Optimisé
# Mettre à jour
composer update
composer update laravel/framework # Un seul package
composer update --lock # Met à jour le lock uniquement
# Lister les dépendances
composer show
composer show --tree # Arbre de dépendances
composer show --platform # Extensions PHP
composer outdated # Versions obsolètes
# Validation
composer validate
composer diagnose
# Nettoyage
composer clear-cache
04.3 Autoloading PSR-4
Structure
project/
├── composer.json
├── src/
│ ├── Models/
│ │ └── User.php # App\Models\User
│ ├── Repositories/
│ │ └── UserRepository.php # App\Repositories\UserRepository
│ └── Services/
│ └── AuthService.php # App\Services\AuthService
└── tests/
├── Unit/ # Tests\Unit\
└── Feature/ # Tests\Feature\
composer.json
{
"autoload": {
"psr-4": {
"App\\": "src/",
"App\\Database\\Migrations\\": "database/migrations/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
Classes
<?php
// src/Models/User.php
namespace App\Models;
class User
{
// Le namespace correspond au chemin src/Models/User.php
}
// Régénération de l'autoloader
composer dump-autoload
composer dump-autoload -o # Optimisé
04.4 Scripts Composer
{
"scripts": {
"test": "phpunit tests/",
"test:coverage": "phpunit tests/ --coverage-html coverage/",
"cs-fix": "php-cs-fixer fix src/",
"stan": "phpstan analyse src/ --level=max",
"all": ["@test", "@cs-fix", "@stan"],
"pre-update-cmd": "echo 'Mise à jour des dépendances...'",
"post-update-cmd": "composer dump-autoload -o",
"post-install-cmd": [
"php artisan migrate",
"php artisan db:seed"
]
}
}
# Exécution
composer test
composer run all
composer run-script post-install-cmd
04.5 Repositories
Repositories personnalisés
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/mon-vendor/mon-package"
},
{
"type": "path",
"url": "./packages/*",
"options": {
"symlink": true
}
},
{
"type": "composer",
"url": "https://satis.example.com"
}
]
}
Private Packagist / Satis
{
"repositories": [
{
"type": "composer",
"url": "https://repo.packagist.com/your-company/"
}
]
}
04.6 PSR-3 — Logger Interface
<?php
declare(strict_types=1);
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\AbstractLogger;
// Le standard PSR-3 définit 8 niveaux de log
// Emergency > Alert > Critical > Error > Warning > Notice > Info > Debug
// Implémentation personnalisée
class FileLogger extends AbstractLogger
{
public function __construct(
private readonly string $logFile = 'app.log',
) {}
public function log(mixed $level, string|\Stringable $message, array $context = []): void
{
$interpolated = $this->interpolate($message, $context);
$line = sprintf(
'[%s] %s: %s%s',
date('c'),
strtoupper((string) $level),
$interpolated,
PHP_EOL
);
file_put_contents($this->logFile, $line, FILE_APPEND);
}
private function interpolate(string $message, array $context): string
{
$replace = [];
foreach ($context as $key => $val) {
$replace['{' . $key . '}'] = $val;
}
return strtr($message, $replace);
}
}
// Utilisation avec Monolog
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('php://stdout', Logger::DEBUG));
$logger->pushHandler(new RotatingFileHandler('logs/app.log', 30, Logger::WARNING));
$logger->info('Utilisateur créé', ['id' => 42, 'name' => 'Alice']);
04.7 PSR-7 — HTTP Messages
<?php
declare(strict_types=1);
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
// PSR-7 définit les interfaces pour les messages HTTP
// RequestInterface, ResponseInterface, ServerRequestInterface,
// StreamInterface, UriInterface, UploadedFileInterface
// Création
$request = new Request('GET', 'https://api.example.com/users');
$request = $request->withHeader('Authorization', 'Bearer token123');
// Réponse
$response = new Response(
status: 200,
headers: ['Content-Type' => 'application/json'],
body: '{"name": "Alice"}',
);
// Middleware PSR-15
interface MiddlewareInterface {
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface;
}
// Utilisation avec Guzzle
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.example.com/',
'timeout' => 5.0,
'headers' => ['Accept' => 'application/json'],
]);
$response = $client->get('/users/42', [
'query' => ['include' => 'posts'],
]);
$user = json_decode($response->getBody(), true);
04.8 PSR-11 — Container Interface
<?php
declare(strict_types=1);
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Container\ContainerExceptionInterface;
// PSR-11 définit l'interface d'un conteneur de dépendances (Service Container)
// Exemple d'implémentation simple
class SimpleContainer implements ContainerInterface
{
private array $bindings = [];
private array $instances = [];
public function set(string $id, callable|object $factory): void
{
$this->bindings[$id] = $factory;
}
public function get(string $id): mixed
{
if (!$this->has($id)) {
throw new class($id) extends \RuntimeException
implements NotFoundExceptionInterface {};
}
if (!isset($this->instances[$id])) {
$factory = $this->bindings[$id];
$this->instances[$id] = is_callable($factory) ? $factory($this) : $factory;
}
return $this->instances[$id];
}
public function has(string $id): bool
{
return isset($this->bindings[$id]);
}
}
// Utilisation
$container = new SimpleContainer();
$container->set(LoggerInterface::class, fn() => new Monolog\Logger('app'));
$container->set(UserRepository::class, fn(ContainerInterface $c) =>
new UserRepository($c->get(LoggerInterface::class))
);
$repo = $container->get(UserRepository::class);
04.9 PSR-14 — Event Dispatcher
<?php
declare(strict_types=1);
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
// PSR-14 standardise la gestion des événements
class UserCreated
{
public function __construct(
public readonly int $userId,
public readonly string $email,
public readonly \DateTimeImmutable $occurredAt = new \DateTimeImmutable(),
) {}
}
class SendWelcomeEmail
{
public function __invoke(UserCreated $event): void
{
echo "Envoi d'email de bienvenue à {$event->email}\n";
}
}
class SimpleEventDispatcher implements EventDispatcherInterface
{
public function __construct(
private ListenerProviderInterface $provider,
) {}
public function dispatch(object $event): object
{
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
return $event;
}
foreach ($this->provider->getListenersForEvent($event) as $listener) {
$listener($event);
}
return $event;
}
}
// Utilisation avec Symfony EventDispatcher ou Laravel Events
04.10 Packages saillants
Carbon (dates)
use Carbon\Carbon;
$now = Carbon::now();
$birthday = Carbon::createFromDate(1990, 5, 15);
echo $birthday->diffForHumans(); // "35 years ago"
echo $now->addDays(10)->format('d/m/Y');
Carbon::setLocale('fr');
echo Carbon::now()->isoFormat('dddd D MMMM YYYY'); // "mardi 29 juillet 2026"
Monolog (logging)
use Monolog\Logger;
use Monolog\Handler\{StreamHandler, SlackWebhookHandler, ChromePHPHandler};
$logger = new Logger('payment');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::WARNING));
$logger->pushHandler(new SlackWebhookHandler('https://hooks.slack.com/...'));
$logger->error('Paiement échoué', ['order_id' => 1234, 'reason' => 'solde insuffisant']);
Guzzle (HTTP)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client([
'base_uri' => 'https://jsonplaceholder.typicode.com/',
'timeout' => 2.0,
'headers' => ['Accept' => 'application/json'],
]);
try {
$response = $client->get('posts/1');
$post = json_decode($response->getBody(), true);
$newPost = $client->post('posts', [
'json' => ['title' => 'Nouveau post', 'body' => '...'],
]);
} catch (RequestException $e) {
if ($e->hasResponse()) {
$status = $e->getResponse()->getStatusCode();
}
}
PHP CS Fixer
<?php
// .php-cs-fixer.dist.php
$finder = PhpCsFixer\Finder::create()
->in(['src', 'tests'])
->exclude('vendor');
$config = new PhpCsFixer\Config();
return $config
->setRules([
'@PSR12' => true,
'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'single_quote' => true,
])
->setFinder($finder);
04.11 Semver et gestion des versions
MAJOR.MINOR.PATCH → 2.1.3
MAJOR : changement incompatible (breaking change)
MINOR : ajout de fonctionnalité (rétrocompatible)
PATCH : correction de bug (rétrocompatible)
Cas particuliers :
^1.2.3 → >=1.2.3 et <2.0.0
~1.2.3 → >=1.2.3 et <1.3.0
1.* → 1.x
* → n'importe quelle version
1.0 → =1.0.0
dev-main → branche de développement
{
"require": {
"laravel/framework": "^10.0 || ^11.0",
"phpunit/phpunit": "~10.5.0",
"monolog/monolog": ">=3.0 <4.0"
}
}
Exercices
- Initialisez un projet Composer avec autoloading PSR-4
- Ajoutez Monolog et écrivez des logs dans un fichier rotatif
- Créez un script Composer personnalisé qui exécute PHPStan
- Implémentez un conteneur PSR-11 simple
- Configurez PHP CS Fixer avec les règles PSR-12
Références
- getcomposer.org/doc
- php-fig.org/psr
- packagist.org