Crea una biblioteca con composer

¿Qué es Composer?

Así como NPM es para JavaScript, Composer es la herramienta dedicada para PHP que te permite gestionar las dependencias de librerías para tus aplicaciones. Si has utilizado un framework como Symfony, definitivamente lo has usado. En realidad, todo desarrollador de PHP lo usa a diario. Si estás en esta página, probablemente te estés preguntando cómo crear una librería que pueda ser fácilmente redistribuida y compartida con la comunidad. Eso es lo que intentaré mostrarte.

Para el bien de nuestro tutorial, tomaré el ejemplo concreto de una corrección que tuve que aplicar a un proyecto recientemente. Realmente me pregunté, "¿Por qué no hay ya una herramienta para arreglar esto fácilmente?". Así que, voy a tomar esta pequeña corrección y convertirla en una librería de Composer y quizás, además de permitirme escribir este artículo, esta librería será útil para alguien más.

Contexto: Heredé un proyecto con un problema de deserialización de datos insertados en una base de datos. El desarrollador tuvo la buena idea de usar serialize en un arreglo. Bueno, no es lo que yo habría hecho, pero así es como está hecha la aplicación y en sí misma no es demasiado problemático. Salvo que aparentemente, los datos pasaron de una DB a otra (mysql->mssql->mysql) generando problemas de codificación. Terminamos con un encantador error de unserialize en el desplazamiento. El error es bastante explícito. El formato de serialize de PHP, que indica el tipo seguido por el número de caracteres, nos permite diagnosticar rápidamente que el número calculado de caracteres no es correcto. Es solo cuestión de recalcularlo y reescribirlo en el archivo con un regex y un callback.

Para recordar, aquí está la documentación sobre el formato https://en.wikipedia.org/wiki/PHP_serialization_format

image-1

Entonces, nuestro error proviene del proceso de deserialización cuando el número de caracteres especificado para una cadena no coincide con lo que decodifica la función unserialize. Así que tienes que recalcular. Por ejemplo:

s:6:"apple";

debería ser:

s:5:"apple";

Es bastante simple, pero perfecto para nuestro ejemplo!

¡Comencemos! Vamos a ensuciarnos las manos.

Requisitos previos

Necesitaremos:
– PHP instalado en CLI en nuestra máquina,
– una cuenta en github,
– una cuenta en packagist
– Composer instalado en nuestra máquina.

Suponiendo que, si estás aquí, es porque ya has instalado PHP y Composer. Si aún no, puedes encontrar muchos recursos en Internet para explicar cómo hacerlo.

Entonces, empecemos por crear nuestro proyecto bajo nuestro editor favorito, para mí es PhpStorm. En esta etapa, deberías tener un directorio vacío.

El siguiente comando te permitirá inicializar tu proyecto de Composer y enriquecerlo con información básica como la Licencia, el nombre, la dirección del repositorio y una breve descripción.

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

Pasarás de un directorio vacío a una arquitectura diseñada para Composer. Es bastante mágico.

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

src: Contiene tu código
vendor: Contiene todas las dependencias. Sí, tu librería podría necesitar otras librerías de Composer para funcionar.
composer.json y composer.lock son archivos con los que seguramente estás familiarizado 😉

Como cualquier buena librería, vamos a probar nuestro código en cada etapa de nuestro desarrollo, y para hacer esto instalaremos, adivina qué... PhpUnit.

composer require --dev phpunit/phpunit ^9.5

Configuraremos PhpUnit con el famoso phpunit.xml y agregaremos las referencias correctas en composer.json.

<?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>

Tu archivo composer debería lucir más o menos así.

{
    "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"
    }
}

Puedes crear el directorio tests y ejecutar el comando composer test. No hay pruebas por el momento, pero esto validará que todo esté configurado correctamente.

mkdir tests
composer test
image-3

Finalmente, es hora de crear nuestra primera prueba ficticia que nos permitirá crear los archivos que necesitamos.

<?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

En este punto, tienes lo que se necesita para comenzar a codificar tu propia librería. En lo que a mí respecta, voy a crear los métodos más pequeños y simples posibles para poder probarlos lo más fácilmente posible. Me gusta bastante el principio de "un método, una responsabilidad", que me permite dividir claramente lo que quiero hacer. Además, asignar una simple responsabilidad a cada uno de mis métodos me permite incluir el contrato/documentación directamente en el nombre. Así, un método "isValid" solo tendrá la responsabilidad de probar si una entrada es válida, etc. Aquí está el esquema de la clase que vamos a usar. De nuevo, es solo para ilustrar nuestro tutorial.

public isValid(string $data): bool

public fixIfInvalid(string $data):? string

public fix(string $data): string

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

Entonces, en orden, llamamos unserialize -> probamos si es inválido -> lo arreglamos -> deserializamos. Muy simple en realidad. Estoy proporcionando el código para el ejemplo, y sigamos adelante.

<?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)
        );
    }
}

Ahora que tenemos nuestro código, lo subiremos a Github. Comenzamos por versionar nuestro proyecto y agregar la lista de archivos que no queremos incluir.

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

Inicializa un repositorio vacío en Github. Ahora podemos subir nuestro código a nuestro repositorio.

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

Ahora que tu código está publicado en Github podemos seguir adelante.

image-5

Crea tu primera versión.

image-6

En la página de crear versión, selecciona "Elige una etiqueta" e introduce "v0.0.1" y haz clic en "crear nueva etiqueta", lo mismo para el nombre de tu versión. Introduce "Versión Inicial" como descripción y haz clic en "Publicar versión".

image-8

Una vez creada, estarás en la página de tu primera versión.

image-9

Nuestro último paso consistirá en referenciar nuestra versión en Packagist para que esté directamente accesible vía Composer.

En la página https://packagist.org/login/ usa la opción "iniciar sesión con Github".

image-10

Envía tu repositorio:

image-11
image-12
image-13

Ahora tienes acceso a tu paquete directamente mediante:

composer require partitech/fix-unserialize

Increíble 😉

Ahora configuraremos Github para notificar a Packagist cualquier cambio en el repositorio.

Recupera tu Token API en Packagist https://packagist.org/profile/. Haz clic en Mostrar Token API y copia tu clave.

image-17

En GitHub, ve a Configuración > Webhooks > Editar.

image-16

Cambia el Secreto pegando tu clave API de Packagist.

image-18
image-20

Y voilà, puedes hacer un push en tu repositorio y verificar los registros en Configuración > Webhooks > Entregas Recientes

image-21

Asegúrate de que todo esté bien en Packagist.

image-22

Ahora sabes cómo crear una librería de Composer y cómo entregarla al resto del mundo 😊