Attention aux dépendances Composer non déclarées

Qu’est ce qu’une dépendance Composer non déclarée ? Quel est le danger ? Comment les détecter ? Réponses dans cet article.

Le problème des dépendances Composer non déclarées

Imaginons que dans votre projet PHP vous souhaitez utiliser la version 1 de jms/serializer. Pour cela rien de plus simple avec Composer :

hubert@lecorche:~/blog$ composer require jms/serializer ^1.0
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 8 installs, 0 updates, 0 removals
  - Installing doctrine/instantiator (1.2.0): Loading from cache
  - Installing doctrine/lexer (1.1.0): Loading from cache
  - Installing doctrine/annotations (v1.7.0): Loading from cache
  - Installing phpoption/phpoption (1.5.0): Loading from cache
  - Installing phpcollection/phpcollection (0.5.0): Loading from cache
  - Installing jms/parser-lib (1.0.0): Loading from cache
  - Installing jms/metadata (1.7.0): Loading from cache
  - Installing jms/serializer (1.14.0): Loading from cache
jms/serializer suggests installing symfony/yaml (Required if you'd like to serialize data to YAML format.)
jms/serializer suggests installing doctrine/collections (Required if you like to use doctrine collection types as ArrayCollection.)
jms/serializer suggests installing doctrine/cache (Required if you like to use cache functionality.)
Writing lock file
Generating autoload files

Vous remarquez que l’installation de cette librairie installe plusieurs dépendances dont phpoption.

Vous découvrez phpoption et décidez alors de l’utiliser dans votre application :

<?php

namespace App\App\Entity;

use PhpOption\Option;

class MyRepository
{
    public function findEntity($id)
    {
        return Option::fromValue($this->em->find(...));
    }

    public function findOrCreateEntity($id)
    {
        return $this->findEntity($id)->getOrElse(new Entity());
    }
}

Quelques mois plus tard, vous décidez de passer à la version 2 de jms/serializer :

hubert@lecorche:~/blog$ composer require jms/serializer ^2.0 --update-with-dependencies
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 13 installs, 2 updates, 3 removals
  - Removing phpoption/phpoption (1.5.0)
  - Removing phpcollection/phpcollection (0.5.0)
  - Removing jms/parser-lib (1.0.0)
  - Installing hoa/exception (1.17.01.16): Loading from cache
  - Installing hoa/event (1.17.01.13): Loading from cache
  - Installing hoa/consistency (1.17.05.02): Loading from cache
  - Installing hoa/visitor (2.17.01.16): Loading from cache
  - Installing hoa/ustring (4.17.01.16): Loading from cache
  - Installing hoa/protocol (1.17.01.14): Loading from cache
  - Installing hoa/zformat (1.17.01.10): Loading from cache
  - Installing hoa/iterator (2.17.01.10): Loading from cache
  - Installing hoa/compiler (3.17.08.08): Loading from cache
  - Installing hoa/regex (1.17.01.13): Loading from cache
  - Installing hoa/math (1.17.05.16): Loading from cache
  - Installing hoa/stream (1.17.02.21): Loading from cache
  - Installing hoa/file (1.17.07.11): Loading from cache
  - Updating jms/metadata (1.7.0 => 2.0.0): Loading from cache
  - Updating jms/serializer (1.14.0 => 2.3.0): Downloading (100%)         
Writing lock file
Generating autoload files

Vous exécutez votre application… et mauvaise surprise… vous rencontrez une erreur PHP :

PHP Fatal error: Uncaught Error: Class ‘PhpOption\Option’ not found in src/App/Entity/MyRepository.php:11

Pourquoi cette erreur ? En observant de plus près la mise à jour de jms/serializer (voir plus haut), on constate l’action suivante réalisée par Composer :

Removing phpoption/phpoption (1.5.0)

En effet, alors que phpoption était une dépendance de la version 1 de jms/serializer , ce n’est plus le cas de la version 2. Composer a donc naturellement supprimé phpoption qui n’est plus utile.

Plus utile ? Pas vraiment, car vous l’utilisez dans votre application ! Mais ça, Composer ne le sait pas.

L’erreur est d’avoir utilisé phpoption sans l’avoir déclaré explicitement via Composer.

Comment trouver les dépendances Composer non déclarées ?

Maintenant que nous avons vu le problème que peuvent poser les dépendances non déclarées, comment savoir si son application en possède ?

Une solution serait de lire chaque ligne de code de l’application et de vérifier que tout élément (classe, fonction, interface, trait…) utilisé d’une librairie externe soit bien déclaré via composer. Mais cette solution est fastidieuse et source d’erreurs.

Heureusement pour nous, la librairie ComposerRequireChecker peut faire le travail pour nous.

Une fois la librairie installée (voir fichier README.md du projet pour les différentes procédures d’installations possibles), on peut procéder à la vérification :

hubert@lecorche:~/blog$ php /path/to/phar/composer-require-checker.phar check /path/to/application/composer.json 
ComposerRequireChecker 2.0.0
The following unknown symbols were found:
+------------------+--------------------+
| unknown symbol   | guessed dependency |
+------------------+--------------------+
| PhpOption\Option |                    |
+------------------+--------------------+

Le résultat est celui attendu : phpoption est bien trouvée comme dépendance non déclarée.

Comment résoudre le problème ?

Une fois les dépendances non déclarées trouvées, la solution pour résoudre le problème est très simple : Les déclarer :

hubert@lecorche:~/blog$ composer require phpoption/phpoption ^1.5
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing phpoption/phpoption (1.5.0): Loading from cache
Writing lock file
Generating autoload files

Comment éviter les dépendances Composer non déclarées ?

Pour conclure, afin d’éviter de rencontrer le problème des dépendances Composer non déclarées, il est important d’avoir les réflexes suivants :

  • de déclarer toute dépendance lors de l’utilisation d’éléments (classe, fonction, interface, trait…) externes à l’application ;
  • de vérifier régulièrement que l’application ne possède aucune dépendance Composer non déclarée. Ceci peut être réalisé grâce à une tâche d’intégration continue par exemple.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Étiquettes : ,