Creando paquetes para PHP

La creación de paquetes es una de las mejores formas para volver a aprovechar código y emplearlo en más de una app o proyecto. También hace que las apps sean más mantenibles. Si arreglamos un bug en nuestro paquete, con composer update actualizamos las apps que lo tengan como dependencia.

Creemos un paquete de ejemplo para revisar el proceso que conlleva la creación y publicación de paquetes PHP. Su funcionalidad será realizar conversiones de unidades de temperatura entre Fahrenheit y Celsius.


Plantilla "package-skeleton-php"

Spatie tiene una plantilla llamada "package-skeleton-php" que nos ofrece un buen punto de inicio. Cuenta con Composer configurado y algunas acciones de Github para ejecutar tests automáticamente y realizar revisiones de estilo en nuestro código base.

El repositorio Github de "package-skeleton-php" cuenta con un botón para comenzar a utilizar la plantilla.

Utilizar plantilla

En la página de creación de repositorio, elegimos el propietario del repositorio, un nombre y una descripción. Finalmente decidimos si la visibilidad del proyecto será pública o privada. Por el momento elegimos privada.

Crear repositorio

Clonamos el repositorio recién creado con el comando git clone:

git clone git@github.com:setdeu/fahrenheit-celsius-conversions.git

La plantilla nos ofrece un script de configuración que realiza los ajustes necesarios para asignar un autor, proveedor y nombre al paquete:

php ./configure.php

Nos pregunta el nombre, email y usuario de Github del autor. Un nombre y un namespace para el proveedor (vendor). Nombre del paquete, un nombre de la clase para el paquete y descripción.

Configurar plantilla

Revisemos qué archivos tenemos en nuestro repositorio a partir de la plantilla ya configurada.

  • .github/ISSUE_TEMPLATE — La plantilla que Github empleará cuando alguien cree una issue.
  • .github/workflows/dependabot-auto-merge.yml — Una acción de Github para hacer un merge automáticamente cuando el bot dependabot encuentre problemas de seguridad en nuestras dependencias npm.
  • .github/workflows/php-cs-fixes.yml — Una acción de Github para ejecutar una revisión de estilos al código base.
  • .github/workflows/run-tests.yml — Una acción de Github para ejecutar la suite de tests.
  • .github/workflows/update-changelog.yml — Una acción de Github para mantener actualizado nuestro archivo CHANGELOG.md en cada versión lanzada.
  • .github/workflows/dependabot.yml — Configuración del bot dependabot.
  • .github/FUNDING.yml — Configuración de Github para aceptar donaciones.
  • src/FahrenheitCelsiusConversionsClass.php — La clase PHP generada por la plantilla al inicializar nuestro paquete.
  • tests/ExampleTest.php — Un test de ejemplo para la suite de tests.
  • tests/Pest.php — Configuración para el framework de tests Pest.
  • .editorconfig — Configuración para los editores de texto que abran el proyecto.
  • .gitattributes — Configuración de Git para ignorar archivos que genera, por ejemplo, la ejecución de la suite de tests.
  • .gitignore — Configuración de Git para ignorar archivos de sistema comunes.
  • .php-cs-fixer.dist.php — Configuración para que haya un estándar de estilo en el código base.
  • .CHANGELOG.md — Información de los cambios que habrá en nuevas versiones del paquete.
  • composer.json — Configuración Composer con detalles del paquete.
  • LICENSE.md – Detalles de la licencia que tiene nuestro paquete. Inicialmente una licencia MIT.
  • phpunit.xml.dist — Configuración de PHPUnit.
  • README.md — Detalles que deberían leerse sobre nuestro paquete e instrucciones de uso.

Convirtiendo Celsius a Fahrenheit

El paquete de ejemplo que estamos creando servirá para convertir unidades Celsius a Fahrenheit. Comencemos a añadir la lógica necesaria para la conversión.

Renombraré el archivo src/FahrenheitCelsiusConversionsClass.php a src/Temperature.php para la clase Temperature. El construct de esta clase aceptará $celsius. Tendrá un método toFahrenheit para hacer la conversión a grados Fahrenheit usando la fórmula F = (C * (9/5)) + 32. Y un método estático celsius para aceptar grados celsius e inicializar la clase:

<?php

namespace Setdeu\FahrenheitCelsiusConversions;

class Temperature
{
    protected float $celsius;

	public function __construct(float $celsius)
	{
        $this->celsius = $celsius;
	}

	public function toFahrenheit(): float
    {
        return ($this->celsius * (9/5)) + 32;
    }

    public static function celsius(float $celsius): self
    {
        return new static($celsius);
    }
}

Generamos un test para asegurarnos de que la conversión sea correcta. Renombraré el archivo tests/ExampleTest.php a src/TemperatureTest.php con el siguiente test de conversión de 100 grados celsius a 212 grados Fahrenheit:

<?php

use Setdeu\FahrenheitCelsiusConversions\Temperature;

test('can convert celsius to fahrenheit', function () {
    $fahrenheit = Temperature::celsius(100)->toFahrenheit();

    expect($fahrenheit)->toEqual(212);
});

Ejecutamos el script test en Composer para verificar que pase el test:

composer test
Ejecución del script test

Ejecutando tests con Github Actions

Ejecutar automáticamente los tests, cada vez que el repositorio recibe un push de código o al crear un pull-request, es bastante útil para mantener el paquete estable.

De hecho, la plantilla que hemos utilizado incluye un workflow de Github para hacerlo posible. Se encuentra en el archivo .github/workflows/run-tests.yml:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: true
      matrix:
        os: [ubuntu-latest, windows-latest]
        php: [8.0]
        stability: [prefer-lowest, prefer-stable]

    name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
          coverage: none

      - name: Setup problem matchers
        run: |
          echo "::add-matcher::${{ runner.tool_cache }}/php.json"
          echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

      - name: Install dependencies
        run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction

      - name: Execute tests
        run: vendor/bin/pest

Para describir en qué momento ejecutar los tests, definimos on: [push, pull_request]. Así, al hacer un push de código o al generar un pull-request Github ejecutará el workflow.

En la sección matrix, se define una matriz para ejecutar el workflow mediante una combinación de variables:

  • os — Define los sistemas operativos que queremos ejecutar, la última versión de Ubuntu y la última versión de Windows.
  • php — Define las versiones de PHP que queremos ejecutar, PHP 8.0 en este caso, pero podemos añadir PHP 8.1 con [8.0, 8.1].
  • stability — Define las versiones de paquetes debe emplear Composer. Con Composer podemos preferir instalar las últimas versiones disponibles de los paquetes con prefer-stable o la versión mínima que se puede instalar con prefer-lowest.

En la sección steps, se definen los pasos para ejecutar los tests para cada combinación de variables en la matriz.

  • Checkout code — Define la creación e inicialización de un contenedor para los tests.
  • Setup PHP — Instala la versión de PHP definida en la matriz e instala una serie de extensiones comunes para PHP.
  • Setup problem matchers — Configura Github Actions para proporcionar errores cuando los tests fallen.
  • Install dependencies — Instala las dependencias con Composer con la estabilidad definida en la matriz.
  • Execute tests — Comando para iniciar la ejecución de los tests con Pest.

Al realizar un commit y push, y dirigirmos a la sección Actions del repositorio, observaremos que el workflow de tests se ha ejecutado.

Ejecución del workflow test

En la página de detalles, observamos que los tests se han ejecutado cuatro veces. Una vez por cada combinación posible. Y en el paso Execute tests vemos el detalle de que los tests fueron exitosos.

Ejecución del test

Si hacemos que nuestro test falle y hacemos un commit. Github Actions mostrará el error de que el test a fallado.

<?php

use Setdeu\FahrenheitCelsiusConversions\Temperature;

test('can convert celsius to fahrenheit', function () {
-    $fahrenheit = Temperature::celsius(100)->toFahrenheit();
+    $fahrenheit = Temperature::celsius(200)->toFahrenheit();

    expect($fahrenheit)->toEqual(212);
});
Test fallido

Revertimos el último cambio para que el test apruebe.

Pull requests

Intentemos la ejecución de los tests en un pull-request. Creamos una branch nueva, git checkout -b test-pr. Hacemos que el test falle y hacemos un commit.

Al crear un Pull request, Github nos mostrará que los tests están fallando. De esta manera podemos verificar que la suite de tests pasen incluso antes de incorporar el código al repositorio.

Test fallido en pull-request

Arreglando el estilo de código con Github Actions

Localmente, podemos arreglar problemas de estilo con el comando composer format, proporcionado por la plantilla que hemos usado. Ejecuta PHP CS Fixer utilizando el archivo de configuración .php-cs-fixer.dist.php.

composer format

Pero también podemos arreglar el estilo de código automáticamente en cada push al repositorio empleando Github Actions. Así, aseguramos una consistencia de estilo, incluso al recibir contribuciones de código.

La plantilla incluye un workflow de Github para hacerlo posible. Se encuentra en el archivo .github/workflows/php-cs-fixer.yml.

name: Check & fix styling

on: [push]

jobs:
  php-cs-fixer:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          ref: ${{ github.head_ref }}

      - name: Run PHP CS Fixer
        uses: docker://oskarstark/php-cs-fixer-ga
        with:
          args: --config=.php-cs-fixer.dist.php --allow-risky=yes

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v4
        with:
          commit_message: Fix styling

Inicializa un contenedor para ejecutar las tareas, ejecuta PHP CS Fixer para arreglar el estilo y realiza un commit "Fix styling" de los cambios realizados.

Al hacer un commit y push, y dirigirmos a la sección Actions del repositorio, observaremos que el workflow de Check & fix styling se ha ejecutado.

Ejecución del workflow Check & fix styling

Lanzamiento del paquete

Lancemos el paquete para que otros usuarios o proyectos puedan instalarlo vía Composer. Lo publicaremos en Packagist, que es el principal repositorio público de paquetes en el ecosistema PHP.

Comencemos con hacer público el repositorio para que Packagist pueda acceder a él.

En la sección de Settings de la página de nuestro repositorio en Github hay una sección para cambiar la visibilidad a público.

Repositorio settings Cambiar visibilidad a público

Ahora, con una cuenta registrada en Packagist enviamos nuestro paquete especificando la URL del repositorio.

Enviar a Packagist

Nuestro paquete ha sido publicado con una única versión dev-main. Es decir, Composer instalará el paquete con siempre hasta el último commit de la branch main.

Paquete publicado

Normalmente, etiquetaremos los lanzamientos por versión. Asegurándonos de que la versión etiquetada es estable para utilizarse.

Modifiquemos nuestro archivo CHANGELOG.md para dar detalles de la versión que estamos por lanzar. Este archivo contendrá nuestro historial de versiones.

Changelog v1.0.0

En Github creemos una nueva realease. Elegimos o generamos una tag. Colocamos la versión 1.0.0 como título para nuestra realease y una descripción.

Crear nueva realease Crear tag Crear realease

Creada nuestra release, Packagist automáticamente detectará la nueva versión y la hará disponible públicamente.

Nueva versión en Packagist

Usando el paquete publicado

Probemos instalar nuestro paquete.

Nos creamos un nuevo directorio de trabajo test:

mkdir test
cd test

Instalamos el paquete vía Composer:

composer require setdeu/fahrenheit-celsius-conversions

Creamos un archivo index.php para realizar una conversión de 100 grados Celsius a Fahrenheit.

<?php

include 'vendor/autoload.php';

use Setdeu\FahrenheitCelsiusConversions\Temperature;

echo Temperature::celsius(100)->toFahrenheit() . PHP_EOL;

Ejecutamos php index.php en nuestro terminal.

php index.php

Fantástico, hemos empleado nuestro paquete y hecho una conversión de prueba.


Publicando una nueva versión

Agreguemos y publiquemos una nueva funcionalidad al paquete para también pueda convertir grados Fahrenheit a celsius.

El construct de Temperature también aceptará $fahrenheit. Tendrá un método toCelsius para hacer la conversión a grados Celsius usando la fórmula C = (F - 32) * (5 / 9). Y un método estático fahrenheit para aceptar grados Fahrenheit e inicializar la clase:

class Temperature
{
    protected float $celsius;
+     protected float $fahrenheit;

-     public function __construct(float $celsius)
+     public function __construct(float $celsius = 0, float $fahrenheit = 0)
    {
        $this->celsius = $celsius;
        $this->fahrenheit = $fahrenheit;
    }

    public function toFahrenheit(): float
        return ($this->celsius * (9 / 5)) + 32;
    }

+     public function toCelsius(): float
+     {
+         return ($this->fahrenheit - 32) * (5 / 9);
+     }

    public static function celsius(float $celsius): self
    {
        return new static($celsius);
    }

+     public static function fahrenheit(float $fahrenheit): self
+     {
+         return new static(0, $fahrenheit);
+     }
}

Generamos un test para asegurarnos de que la conversión sea correcta. En src/TemperatureTest.php añadimos el siguiente test de conversión de 212 grados Fahrenheit a 100 grados Celsius:

// ...

test('can convert fahrenheit to celsius', function () {
    $celsius = Temperature::fahrenheit(212)->toCelsius();

    expect($celsius)->toEqual(100);
});

Ejecutando nuestros tests verificamos que todo esté correcto.

composer test
composer test

Realizamos un commit y hacemos push al repositorio. También podemos comprobar en los Actions de Github estén pasando los tests.

Github actions

Para publicar una nueva version, comenzamos con actualizar el archivo CHANGELOG.md para dar detalles de la versión que estamos por publicar.

Changelog para 1.1.0

La versión será 1.1.0, esto es porque seguiremos las convenciones del versionado Semantic Versioning. Donde incrementos del último dígito 1.0.x representan arreglo de bugs. Incrementos del segundo dígito 1.x.0 representan nuevas funcionalidades añadidas sin introducir cambios de ruptura o breaking changes. Incrementos del primer dígito x.0.0 representan versiones que incluyen cambios de ruptura.

Creamos una nueva realease en Github:

Creando realease 1.1.0 Crear realease 1.1.0

Con esto, Packagist recogerá nuestra última versión publicada y la hará disponible públicamente.

Realease 1.1.0 en Packagist

Probemos nuestra nueva funcionalidad en nuestro proyecto de prueba.

Ejecutamos composer update para actualizar nuestras dependencias y obtener las últimas versiones.

composer update

En nuestro archivo index.php realizamos una conversión de 212 grados Fahrenheit a Celsius.

// ...

echo Temperature::fahrenheit(212)->toCelsius() . PHP_EOL;

Y ejecutamos php index.php en nuestro terminal.

composer update

Utilizando las issues y discussions de GitHub

Las funcionalidades de issues y discussions de GitHub son una excelente forma de estar abierto a discusiones y contribuciones. En nuestro repositorio contamos con un archivo .github/ISSUE_TEMPLATE/config.yml que configura cómo debe abrirse una issue en Github.

blank_issues_enabled: false
contact_links:
    - name: Ask a question
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/discussions/new?category=q-a
      about: Ask the community for help
    - name: Request a feature
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/discussions/new?category=ideas
      about: Share ideas for new features
    - name: Report a security issue
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/security/policy
      about: Learn how to notify us for sensitive bugs
    - name: Report a bug
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/issues/new
      about: Report a reproducable bug

Con la opción de blank_issues_enabled se deshabilita la creación de una issue en blanco. En su lugar, se mostrarán enlaces. Los primeros cuatro enlaces son para abrir un tema de discusión. La última, para crear una issue si se está reportando un bug.

Plantilla issue
  • Hacer una pregunta. La gente puede hacernos cualquier pregunta abriendo una nueva discusión.
  • Solicitar una funcionalidad. La gente puede sugerirnos añadir una nueva funcionalidad abriendo una nueva discusión.
  • Reportar un problema de seguridad. Esto lleva a la página de seguridad donde se pide a la gente que envíe un email en vez de hacer pública algún problema potencial de seguridad.
  • Reportar un bug. Crear una issue donde la gente puede hacernos saber de algún bug con información de cómo reproducirlo.

En desarrollo...