Créer une librairie avec composer

Composer c’est quoi ?

A l’image de NPM pour javascript , composer est l’outil dédié à PHP vous permettant de gérer les dépendances de librairies de vos applications. Si vous utilisez un framework du type Symfony vous l’avez obligatoirement utilisé. En réalité tout développeur PHP l’utilise quotidiennement. Si vous êtes sur cette page c’est que probablement vous vous demandez comment on crée une librairie que l’on peut redistribuer facilement et partager avec la communauté. C’est ce que je vais tenter de vous montrer.

Pour le bien de notre tutoriel je vais prendre un exemple concret d’une correction que j’ai dû appliquer sur un projet récemment. Je me suis vraiment posé la question « Pourquoi y’a pas déjà un outil pour corriger cela facilement ? ». Je vais donc prendre cette petite correction et la transformer en librairie composer et peut-être qu’en plus de me permettre d’écrire cet article cette librairie servira à quelqu’un.

Contexte : Je récupère un projet sur lequel il y a un problème de dé-sérialisation de données insérées dans une base de données. Le développeur a eu la bonne idée d’utiliser serialize sur un array. Bon, c’est pas ce que j’aurais fait, mais l’appli est ainsi faite et en soit ce n’est pas trop dérangeant. Sauf que visiblement les données sont passées d’une BDD à une autre (mysql->mssql->mysql) générant des problèmes d’encodage. On se retrouve au final avec une belle erreur unserialize error at offset. L’erreur est assez verbeuse. Le format serialize de PHP qui nous indique le type, suivi du nombre de caractères, nous permet de rapidement diagnostiquer que le nombre de caractères calculés n’est pas bon. Il suffit juste de recalculer et de l’écrire dans le fichier avec une regex et un callback.

Pour rappel voici la doc du format https://en.wikipedia.org/wiki/PHP_serialization_format

image-1

Notre erreur provient donc du fait qu’à la dé-sérialisation le nombre de caractères spécifiés pour une string ne correspond pas à ce qui est décodé par la fonction unserialize. Il faut donc recalculer. Par exemple :

s:6:"apple";

devrait être :

s:5:"apple";

C’est tout bête, mais parfait pour notre exemple !

Allez c’est partit ! On va mette un peu les mains dans le cambouis.

Les pré-requis

Nous allons avoir besoin de :
– PHP installé en CLI sur notre machine,
– d’un compte sur github,
– d’un compte sur packagist
– de composer installé sur notre machine.

A priori si vous êtes ici c’est que vous avez déjà installé PHP ainsi que composer. Si ce n’est pas encore le cas, vous trouverez tout un tas de ressources sur Internet pour vous expliquer comment.

On va donc commencer par se créer son projet sous son éditeur préféré, pour ma part : PhpStorm. A ce stade vous devriez avoir un répertoire vide.

La commande suivante va vous permettre d’initialiser votre projet composer et de l’agrémenter d’informations de base comme la License, le nom, l’adresse de votre dépôt ainsi que d’une petite description.

composer init -q -a src \
--name partitech/fix-serialize \
--description "Fix Php unserialize error at offset" \
--license MIT

Vous allez passer d’un coup d’un répertoire vide à une architecture prévue pour composer. C’est assez magique.

image-2
{
    "name": "partitech/fix-serialize",
    "description": "Fix Php unserialize error at offset",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "Partitech\\FixSerialize\\": "src"
        }
    },
    "require": {}
}

src : Contient votre code
vendor : Contient toutes les dépendances. Oui votre librairie peut avoir besoin d’autres librairies composer pour fonctionner.
composer.json et composer.lock sont des fichiers auxquels vous êtes forcément habitués 🙂

Comme toute bonne librairie nous allons tester à chaque étape de notre développement notre code et pour cela nous allons installer, je vous le donne en mille…. PhpUnit.

composer require --dev phpunit/phpunit ^9.5

Nous allons configurer PhpUnit avec le fameux phpunit.xml et ajouter dans composer.json les bonnes références.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="common tests">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Votre fichier composer devrait sensiblement ressembler à cela.

{
    "name": "partitech/fix-unserialize",
    "description": "Fix Php unserialize error at offset",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "Partitech\\FixUnSerialize\\": "src"
        }
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5"
    },
    "scripts": {
        "test": "phpunit"
    }
}

Vous pouvez créer le répertoire tests et lancer la commande composer test. Il n’y a aucun test pour le moment mais cela validera que l’ensemble est correctement créé.

mkdir tests
composer test
image-3

Il est enfin temps de créer notre premier test factice ce qui va nous permettre de créer les fichiers dont nous avons besoin.

<?php

namespace Partitech\FixUnSerialize\Tests;

use Partitech\FixUnSerialize\UnSerializeService;

class FixUnSerializeTest extends \PHPUnit\Framework\TestCase
{
    public function testSayHelloWorld()
    {
        $helloWorld = new UnSerializeService();
        $this->assertEquals("Hello World!", $helloWorld->unserialize('Hello'));
    }
}
<?php
namespace Partitech\FixUnSerialize;

class UnSerializeService
{
    /**
     * @param string $data
     * @return string|null
     */
    public function unserialize(string $data): ?string
    {
        return $data . ' World!';
    }
}
image-4

A ce stade vous avez ce qu’il faut pour commencer à coder votre propre librairie. En ce qui me concerne je vais créer des méthodes les plus petites et simples possibles afin de pouvoir les tester le plus simplement. J’aime assez le principe de « une méthode une responsabilité » me permettant de bien découper ce que je souhaite faire. De plus le fait d’assigner une responsabilité simple à chacune de mes méthodes me permet d’inclure le contrat/documentation directement dans le nom. Ainsi une méthode « isValid » n’aura que la responsabilité de tester si une entrée est valide, etc. Voici le schéma de la classe que nous allons utiliser. Encore une fois ceci n’est là que pour illustrer notre tutoriel.

public isValid(string $data): bool

public fixIfInvalid(string $data):? string

public fix(string $data): string

public fixLength(array $values): string
 
public unserialize(string $data) 

Donc dans l’ordre on appelle unserialize -> on test si invalide -> on fixe -> on dé-serialize. En somme très simple. Je vous fournis le code pour l’exemple et on passe à la suite.

<?php

namespace Partitech\FixUnSerialize;

class UnSerializeService
{
    /**
     * @param string $data
     * @return mixed
     */
    public function unserialize(string $data)
    {
        $data = $this->fixIfInvalid($data);
        return unserialize($data);
    }

    /**
     * @param string $data
     * @return string|null
     */
    public function fixIfInvalid(string $data): ?string
    {
        if (!$this->isValid($data)) {
            $data = $this->fix($data);
        }
        return $data;
    }

    /**
     * @param string $data
     * @return bool
     */
    public function isValid(string $data): bool
    {
        if (!@unserialize($data)) {
            return false;
        }

        return true;
    }

    /**
     * @param string $data
     * @return string
     */
    public function fix(string $data): string
    {
        $pattern = '/s\:(\d+)\:\"(.*?)\";/s';
        return preg_replace_callback($pattern, [$this, 'fixLength'], $data);
    }

    /**
     * @param array $values
     * @return string
     */
    public function fixLength(array $values): string
    {
        $string = $values[2];
        $length = strlen($string);
        return 's:' . $length . ':"' . $string . '";';
    }
}
<?php

namespace Partitech\FixUnSerialize\Tests;

use Partitech\FixUnSerialize\UnSerializeService;
use PHPUnit\Framework\TestCase;

class FixUnSerializeTest extends TestCase
{
    const VALID_STRING = 'a:2:{s:4:"test";s:4:"test";s:5:"test2";s:5:"test2";}';
    const INVALID_STRING = 'a:2:{s:123456:"test";s:4:"test";s:5:"test2";s:5:"test2";}';

    private UnSerializeService $unserializeService;

    public function __construct(?string $name = null, array $data = [], $dataName = '')
    {
        parent::__construct($name, $data, $dataName);
        $this->unserializeService = new UnSerializeService();
    }

    public function testIsValidTrue()
    {
        $this->assertTrue($this->unserializeService->isValid(self::VALID_STRING));
    }

    public function testIsValidFalse()
    {
        $this->assertNotTrue($this->unserializeService->isValid(self::INVALID_STRING));
    }

    public function testFixIfInvalidWithValidString()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fixIfInvalid(self::VALID_STRING)
        );
    }

    public function testFixIfInvalidWithInvalidString()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fixIfInvalid(self::INVALID_STRING)
        );
    }

    public function testFixLength()
    {
        $data = [
            's:5000:"test2";',
            5,
            'test2'
        ];
        $expected = 's:5:"test2";';
        $this->assertEquals(
            $expected,
            $this->unserializeService->fixLength($data)
        );
    }

    public function testFixValid()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fix(self::VALID_STRING)
        );
    }

    public function testFixInvalid()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fix(self::INVALID_STRING)
        );
    }

    public function testUnserializeValid()
    {
        $this->assertEquals(
            unserialize(self::VALID_STRING),
            $this->unserializeService->unserialize(self::VALID_STRING)
        );
    }

    public function testUnserializeInvalid()
    {
        $this->assertEquals(
            unserialize(self::VALID_STRING),
            $this->unserializeService->unserialize(self::INVALID_STRING)
        );
    }
}

Maintenant que nous avons notre code nous allons le pousser sur Github. On commence par versionner notre projet et ajouter la liste des fichiers que nous ne souhaitons pas inclure.

git init
touch .gitignore
echo "vendor/" >> .gitignore
echo ".phpunit.result.cache" >> .gitignore
echo ".idea/" >> .gitignore
git add .
git commit -m "First commit"

Initialisez un dépôt vide sur Github. Nous pouvons à présent pousser notre code dans notre dépôt.

git remote add origin ssh://git@github.com/partitech/fix-serialize.git
git branch -M main
git push -f -u origin main

Maintenant que votre code est publié sur Github nous pouvons passer à la suite.

image-5

Créez votre première release.

image-6

Dans la page de création de release sélectionnez « Choose a tag » et entrez « v0.0.1 » et cliquez sur « create new tag« , idem pour le nom de votre release. Entrez « Initial release » comme description et cliquez sur « Publish release« .

image-8

Une fois créé vous vous retrouvez sur votre première page de release.

image-9

Notre dernière étape va consister à référencer notre version sur packagist afin d’y avoir accès directement via composer.

Sur la page https://packagist.org/login/ utilisez l’option « log with Github« .

image-10

Soumettez votre dépôt :

image-11
image-12
image-13

Vous avez maintenant accès à votre package directement via :

composer require partitech/fix-unserialize

Génial 🙂

Nous allons maintenant configurer Github pour notifier Packagist de tout changement sur le dépôt.

Récupérez votre Api Token sur Packagist https://packagist.org/profile/. Clikez sur Show API Token et copiez votre clef.

image-17

Dans GitHub, allez dans Settings > Webhooks > Edit.

image-16

Changez le Secret en collant votre clef d’API Packagist.

image-18
image-20

Et voila, vous pouvez faire un push sur votre dépôt et regarder les logs Settings > Webhooks > Recent Deliveries

image-21

Bien vérifier sur Packagist que tout est ok.

image-22

Vous savez à présent comment créer une librairie composer et comment la délivrer au reste du monde 😉