Aller au contenu principal
Retour au blog

PHP 8.5 : pipe operator, extension URI et #[NoDiscard] avant la GA

Flavien Métivier8 juillet 20258 min

Le 20 novembre 2025, PHP 8.5 sera en disponibilité générale. À mi-juillet, la branche PHP-8.5 est déjà active sur GitHub et les trois RFC qui vont changer l'écriture du code Symfony au quotidien sont acceptées et implémentées. Autant les décortiquer maintenant, pendant qu'il reste quatre mois pour adapter ses habitudes — plutôt que de devoir se rattraper en urgence le jour de la migration.

PHP 8.5 en juillet 2025 : où en est la branche de développement ?

Contrairement aux cycles précédents où certaines RFC s'éternisaient jusqu'à la RC, PHP 8.5 arrive avec un périmètre défini tôt. La GA respecte le calendrier annuel habituel : 20 novembre 2025. À cette date, PHP 8.3 passera en fin de support actif et PHP 8.4 — sorti en novembre 2024 — restera maintenu jusqu'en 2027. Les équipes sur Symfony 7.3 (version courante depuis le 29 mai 2025) ont donc une fenêtre confortable pour préparer la transition. Les RFC ci-dessous sont à l'état acceptée et implémentée sur la branche de développement, selon php.watch/versions/8.5.

  • Pipe operator `|>` — RFC acceptée, implémentation fusionnée sur la branche dev
  • Extension URI native — RFC acceptée, extension bundled en cours de finalisation
  • Attribut `#[NoDiscard]` — RFC acceptée, implémentation dans le core
  • Autres ajouts notables : `array_first()` / `array_last()` natives, `#[Deprecated]` en core (complément PHP 8.4), lazy objects améliorés

Le pipe operator |> : enchaîner sans polluer le scope

Le pipe operator est probablement la feature la plus attendue dans la communauté PHP depuis des années. Le principe est simple : la valeur à gauche de |> est passée comme premier argument au callable à droite. On chaîne des transformations de gauche à droite, dans l'ordre de lecture naturel — à l'opposé des appels imbriqués qui se lisent de droite à gauche. Les fonctions dont la valeur principale est en première position (strtolower, trim, strip_tags…) s'utilisent directement avec la syntaxe first-class callable .... Pour les autres, une closure courte suffit.

<?php
// PHP 8.4 — imbrication illisible ou variables intermédiaires parasites
$slug = trim(preg_replace('/[^a-z0-9]+/', '-', strtolower(strip_tags($title))), '-');

// PHP 8.4 alternatif — variables temporaires qui polluent le scope
$step1 = strip_tags($title);
$step2 = strtolower($step1);
$step3 = preg_replace('/[^a-z0-9]+/', '-', $step2);
$slug  = trim($step3, '-');

// PHP 8.5 — pipeline lisible, de gauche à droite, zéro variable intermédiaire
$slug = $title
    |> strip_tags(...)
    |> strtolower(...)
    |> fn(string $s): string => preg_replace('/[^a-z0-9]+/', '-', $s)
    |> fn(string $s): string => trim($s, '-');

Dans un projet Symfony 7.3, ce pattern est immédiatement utile dans les DataTransformer, les EventSubscriber ou les normalizers. Exemple concret avec un transformer de formulaire :

<?php
// src/Form/DataTransformer/SlugTransformer.php

namespace App\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

final class SlugTransformer implements DataTransformerInterface
{
    public function transform(mixed $value): string
    {
        if (!is_string($value)) {
            return '';
        }

        // PHP 8.5 : pipeline explicite, chaque étape est nommée par sa position
        return $value
            |> strip_tags(...)
            |> fn(string $s): string => preg_replace('/\s+/', ' ', $s)
            |> trim(...)
            |> strtolower(...)
            |> fn(string $s): string => preg_replace('/[^a-z0-9\s-]/', '', $s)
            |> fn(string $s): string => preg_replace('/[\s-]+/', '-', $s)
            |> fn(string $s): string => trim($s, '-');
    }

    public function reverseTransform(mixed $value): string
    {
        return is_string($value) ? $value : '';
    }
}

L'extension URI native : adieu parse_url(), bonjour les objets immuables

parse_url() est l'une des fonctions les plus utilisées et les plus frustratantes de PHP. Elle retourne un tableau associatif partiel — les clés absentes le restent, sans valeur par défaut — ne valide rien, et oblige à enchaîner les isset(). L'extension URI native de PHP 8.5 apporte enfin une API orientée objet, immutable, et qui lève des exceptions sur les URIs malformées.

<?php
// PHP 8.4 — fragile, tableaux partiels, validation manuelle
function extractApiBase(string $url): string
{
    $parts = parse_url($url);

    if ($parts === false || !isset($parts['scheme'], $parts['host'])) {
        throw new \InvalidArgumentException('URL invalide : ' . $url);
    }

    $port = isset($parts['port']) ? ':' . $parts['port'] : '';

    return $parts['scheme'] . '://' . $parts['host'] . $port;
}
<?php
// PHP 8.5 — API orientée objet, immutable, exceptions explicites
use Uri\Uri;
use Uri\InvalidUriException;

function extractApiBase(string $url): string
{
    try {
        $uri = Uri::new($url);
    } catch (InvalidUriException $e) {
        throw new \InvalidArgumentException('URL invalide : ' . $url, previous: $e);
    }

    // Propriétés readonly typées — jamais de tableau partiel ni de null surprise
    return $uri->withPath('')->withQuery('')->withFragment('')->toString();
}

// Manipulation complète d'une URI
$uri = Uri::new('https://api.example.com:8443/v2/users?page=2&sort=asc#top');

echo $uri->scheme;    // https
echo $uri->host;      // api.example.com
echo $uri->port;      // 8443
echo $uri->path;      // /v2/users
echo $uri->query;     // page=2&sort=asc
echo $uri->fragment;  // top

// Chaque wither retourne une nouvelle instance — immutabilité garantie
$nextPage = $uri->withQuery('page=3&sort=asc');
echo $nextPage; // https://api.example.com:8443/v2/users?page=3&sort=asc#top

L'avantage structurant pour Symfony : Uri\Uri est désormais un vrai type utilisable dans les signatures de méthodes. Tu remplaces les paramètres string $url — qui peuvent contenir n'importe quoi — par Uri\Uri $endpoint, et la validation remonte à l'appelant. Dans les services HTTP, les webhooks handlers ou les normalizers, c'est un gain de robustesse immédiat sans couche d'abstraction supplémentaire. Note : l'API exacte de l'extension peut encore évoluer d'ici les RC de septembre — suivre les changelogs sur php.watch.

#[NoDiscard] : le filet de sécurité que tu n'avais pas

Besoin d'un expert Symfony ?

Réserver un appel

Combien de fois as-tu vu — ou écrit — un $service->save($entity) sans vérifier le résultat, ou un appel à une méthode de validation dont la valeur de retour disparaît dans le vide ? L'attribut #[NoDiscard] marque les fonctions et méthodes dont le retour ne doit pas être silencieusement ignoré. Dès PHP 8.5, appeler une telle méthode sans utiliser sa valeur de retour déclenche un E_WARNING. C'est le même filet de sécurité que [[nodiscard]] en C++ ou #[must_use] en Rust, enfin disponible en PHP.

<?php
// src/Domain/Payment/PaymentService.php

namespace App\Domain\Payment;

use App\Domain\Payment\Result\PaymentResult;

final class PaymentService
{
    /**
     * Retourne le résultat du paiement.
     * Ignorer cette valeur est une erreur métier.
     */
    #[NoDiscard('Le résultat contient le transactionId et le statut — ne pas l\'ignorer.')]
    public function charge(int $amountCents, string $customerId): PaymentResult
    {
        // ... logique de paiement ...
        return new PaymentResult(
            success: true,
            transactionId: 'txn_' . bin2hex(random_bytes(8)),
        );
    }
}

// --- Utilisation ---

$service = new PaymentService();

// ✗ PHP 8.5 : Warning — return value of charge() should not be discarded
$service->charge(4990, 'cust_abc123');

// ✓ Correct : résultat exploité
$result = $service->charge(4990, 'cust_abc123');

if (!$result->success) {
    throw new PaymentFailedException($result->errorMessage ?? 'Erreur inconnue');
}

logger()->info('Paiement effectué', ['txn' => $result->transactionId]);

L'attribut joue le même rôle que les types stricts : il transforme des erreurs silencieuses en erreurs détectables — en phase de développement, pas en production. Sur un grand codebase Symfony avec des dizaines de services, les candidats naturels à #[NoDiscard] sont toutes les méthodes qui retournent des value objects critiques : résultats de validation, résultats d'opérations d'écriture, résultats d'appels HTTP internes. Tu peux combiner l'attribut avec PHPStan pour le détecter aussi statiquement dès aujourd'hui via phpstan-strict-rules.

Les trois features combinées dans un Command handler Symfony réel

Voici un Command handler d'import qui combine les trois nouveautés. C'est volontairement condensé pour montrer comment elles se complètent — dans un vrai projet, le service UserImporter serait dans sa propre classe avec ses propres tests.

<?php
// src/Command/ImportUsersCommand.php

namespace App\Command;

use App\Service\UserImporter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Uri\Uri;
use Uri\InvalidUriException;

#[AsCommand(name: 'app:import-users')]
final class ImportUsersCommand extends Command
{
    public function __construct(
        private readonly UserImporter $importer,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->addArgument('source-url', InputArgument::REQUIRED, 'URL de la source CSV');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // Extension URI — validation immédiate, typage fort
        try {
            $sourceUri = Uri::new($input->getArgument('source-url'));
        } catch (InvalidUriException $e) {
            $output->writeln('<error>URL invalide : ' . $e->getMessage() . '</error>');
            return Command::FAILURE;
        }

        // Pipe operator — normalisation du path sans variables temporaires
        $normalizedPath = $sourceUri->path
            |> fn(string $p): string => trim($p, '/')
            |> strtolower(...)
            |> fn(string $p): string => '/' . $p;

        $cleanUri = $sourceUri->withPath($normalizedPath);

        // #[NoDiscard] sur UserImporter::run() — le compilateur force à exploiter le résultat
        $result = $this->importer->run($cleanUri);

        $output->writeln(sprintf(
            '<info>Import terminé : %d lignes traitées, %d erreurs</info>',
            $result->processed,
            $result->errors,
        ));

        return $result->errors > 0 ? Command::FAILURE : Command::SUCCESS;
    }
}

Préparer ta codebase avant la GA de novembre

Il reste quatre mois. C'est confortable à condition de ne pas attendre le jour J pour regarder ce qui casse.

  • Active `declare(strict_types=1)` partout si ce n'est pas encore fait — les nouvelles features amplifient les bénéfices du mode strict
  • Audite tes `parse_url()` avec `grep -rn "parse_url" src/` pour identifier les migrations vers l'extension URI
  • Identifie les méthodes qui retournent des value objects critiques (validation, paiement, import) comme candidats naturels à `#[NoDiscard]`
  • Ajoute `8.5` dans ta matrice CI dès la première RC (prévue septembre 2025) sans retirer 8.4
  • Déclare la dépendance PHP avec `>=8.4` plutôt que `^8.4` pour faciliter la transition sur Composer
  • Monte PHPStan à `level: max` et `phpVersion: 80400` maintenant — tu basculeras à `80500` quand la branche sera verte en CI
{
    "require": {
        "php": ">=8.4",
        "symfony/framework-bundle": "^7.3"
    },
    "config": {
        "platform": {
            "php": "8.4.0"
        }
    }
}
# phpstan.neon
parameters:
    level: max
    phpVersion: 80400  # Monter à 80500 quand la branche CI 8.5-rc est verte
    paths:
        - src
        - tests

# .github/workflows/ci.yml (extrait matrice)
jobs:
  tests:
    strategy:
      matrix:
        php: ['8.4', '8.5']  # Ajouter 8.5 dès les RC de septembre 2025
    steps:
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: uri  # Déclarer l'extension URI explicitement

PHP 8.5 n'est pas une révolution, c'est une maturation

Le pipe operator clarifie les pipelines de transformation sans introduire de nouveaux concepts. L'extension URI donne enfin au traitement des URLs la rigueur qu'il mérite depuis des années — là où PSR-7 n'avait apporté cette robustesse qu'aux seules stacks HTTP complètes. #[NoDiscard] transforme des erreurs silencieuses — parmi les plus coûteuses en production — en avertissements détectables dès le développement. Ces trois features ne changent pas l'architecture de tes projets Symfony : elles rendent le code que tu écris déjà plus lisible, plus robuste et plus explicite sur ses intentions. C'est exactement ce qu'on attend d'une version mineure bien construite.

Cet article vous a plu ? Partagez-le !

Besoin d'un expert Symfony ?

20 ans d'expérience sur l'écosystème PHP/Symfony.