MFormations
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

  1. Initialisez un projet Composer avec autoloading PSR-4
  2. Ajoutez Monolog et écrivez des logs dans un fichier rotatif
  3. Créez un script Composer personnalisé qui exécute PHPStan
  4. Implémentez un conteneur PSR-11 simple
  5. Configurez PHP CS Fixer avec les règles PSR-12

Références

  • getcomposer.org/doc
  • php-fig.org/psr
  • packagist.org