Categorías
Laravel PHP Symfony

Guía Maestra: Arquitectura, Testing y Publicación de tu propio SDK en PHP


En el mundo de «PHP y otros pokemones», hay dos tipos de entrenadores: los que usan las herramientas y los que las forjan. Ya sabes consumir una API, eso es básico. Pero, ¿alguna vez te has encontrado copiando y pegando las mismas llamadas a la API en tres proyectos diferentes?

Ese es el momento de evolucionar. Es hora de encapsular esa lógica en un SDK (Software Development Kit).

En este post, no vamos a hacer un script sencillo. Vamos a construir un SDK profesional, testeable, agnóstico y listo para Packagist. Prepárate, porque este viaje es largo.


1. Los Cimientos: Estructura y Composer

Un SDK profesional comienza con una estructura impecable. No tires todo en la carpeta raíz. Usaremos el estándar PSR-4 para la carga automática de clases.

Estructura de directorios recomendada:

mi-sdk-pokemon/
├── src/
│   ├── Client.php          # El corazón del SDK
│   ├── Resources/          # Agrupación lógica de endpoints
│   ├── DTOs/               # Data Transfer Objects
│   ├── Exceptions/         # Excepciones personalizadas
│   └── Contracts/          # Interfaces
├── tests/                  # Aquí viven tus pruebas
├── .github/
│   └── workflows/          # CI/CD (GitHub Actions)
├── composer.json
└── README.md

En tu composer.json, define tu namespace. Esto es tu identidad:

{
    "name": "tu-usuario/poke-sdk",
    "description": "Un SDK robusto para la PokeAPI",
    "type": "library",
    "autoload": {
        "psr-4": {
            "PokeTools\\Sdk\\": "src/"
        }
    },
    "require": {
        "php": "^8.2",
        "guzzlehttp/guzzle": "^7.8"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0",
        "phpstan/phpstan": "^1.10"
    }
}


2. El Corazón: Cliente Agnóstico e Inyección de Dependencias

El error más común es crear una dependencia fuerte con la librería HTTP (como Guzzle). Tu SDK debe ser flexible.

Diseñamos la clase principal (PokeClient) permitiendo que el usuario inyecte su propia configuración (proxies, loggers, middlewares).

namespace PokeTools\Sdk;

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface;

class PokeClient
{
    protected ClientInterface $httpClient;

    // Inyección de dependencias: Clave para el testing
    public function __construct(?ClientInterface $client = null, string $apiKey = '')
    {
        $this->httpClient = $client ?? new GuzzleClient([
            'base_uri' => 'https://pokeapi.co/api/v2/',
            'headers' => [
                'Authorization' => "Bearer {$apiKey}",
                'Accept'        => 'application/json',
            ]
        ]);
    }

    public function getHttpClient(): ClientInterface
    {
        return $this->httpClient;
    }
}


3. Arquitectura de Recursos (Fluent Interface)

No crees una «God Class» con 50 métodos (getPokemon, getBerry, getItem, getRegion…). Eso es inmanteneble. Usa el patrón Resource.

El cliente principal actúa como una fábrica que despacha recursos específicos.

// src/Resources/PokemonResource.php
namespace PokeTools\Sdk\Resources;

use PokeTools\Sdk\DTOs\PokemonData;

class PokemonResource extends BaseResource
{
    public function find(string $name): PokemonData
    {
        // Lógica encapsulada
        $response = $this->client->request('GET', "pokemon/{$name}");
        
        return PokemonData::fromArray(json_decode($response->getBody(), true));
    }
}

// src/PokeClient.php (Añadimos esto)
public function pokemon(): Resources\PokemonResource
{
    return new Resources\PokemonResource($this->httpClient);
}

El resultado para el usuario final es elegante:

$sdk->pokemon()->find('pikachu');


4. La Joya de la Corona: DTOs (Data Transfer Objects)

Aquí es donde demuestras que eres un Senior. Nunca devuelvas arrays asociativos crudos.

Los arrays son cajas negras. Un DTO es un contrato claro de qué datos existen. Además, con las nuevas features de PHP 8.2+, son hermosos de escribir.

namespace PokeTools\Sdk\DTOs;

readonly class PokemonData
{
    public function __construct(
        public int $id,
        public string $name,
        public int $height,
        public array $types
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            id: $data['id'],
            name: $data['name'],
            height: $data['height'],
            types: $data['types']
        );
    }
}

Ventaja: El IDE del usuario autocompletará $pokemon->name. Si la API cambia, solo actualizas el DTO y no rompes la implementación de todos.


5. Control de Daños: Excepciones Personalizadas

Si Guzzle falla, lanza una GuzzleHttp\Exception\ClientException. Si tu usuario ve eso, pensará que es culpa de Guzzle. Debes capturar ese error y lanzar uno tuyo, con tu dominio.

// src/Exceptions/PokemonEscapedException.php
class PokemonEscapedException extends \Exception {}

// En tu Resource:
try {
    $response = $this->client->request(...)
} catch (\GuzzleHttp\Exception\ClientException $e) {
    if ($e->getCode() === 404) {
        throw new PokemonEscapedException("No pudimos encontrar a ese Pokémon.");
    }
    throw $e;
}


6. Testing: El gimnasio de entrenamiento

Un SDK sin tests no vale nada. Pero ojo: No hagas peticiones reales a la API en tus tests. Es lento y si la API se cae, tus tests fallan (falso negativo).

Usaremos Mocking. Guzzle tiene un MockHandler maravilloso para esto.

// tests/PokemonResourceTest.php
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use PokeTools\Sdk\PokeClient;

class PokemonResourceTest extends TestCase
{
    public function test_can_fetch_pokemon_data()
    {
        // 1. Preparamos la respuesta falsa (El muñeco de prueba)
        $mock = new MockHandler([
            new Response(200, [], json_encode(['id' => 25, 'name' => 'pikachu', 'height' => 4, 'types' => []]))
        ]);
        $handlerStack = HandlerStack::create($mock);
        
        // 2. Inyectamos el cliente "trucado" en nuestro SDK
        $client = new Client(['handler' => $handlerStack]);
        $sdk = new PokeClient($client);

        // 3. Ejecutamos
        $pokemon = $sdk->pokemon()->find('pikachu');

        // 4. Verificamos (Asserts)
        $this->assertEquals('pikachu', $pokemon->name);
        $this->assertInstanceOf(\PokeTools\Sdk\DTOs\PokemonData::class, $pokemon);
    }
}


7. Automatización: GitHub Actions (CI/CD)

No confíes en tu memoria. Configura un «Juez Automático» que corra tus tests cada vez que subas código o alguien te envíe un Pull Request.

Crea el archivo .github/workflows/tests.yml:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: [8.2, 8.3] # Probamos en varias versiones

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

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}

      - name: Install dependencies
        run: composer install --prefer-dist --no-interaction

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

Ahora, cada vez que hagas un git push, GitHub te mostrará un check verde si tu código es digno de un maestro o una cruz roja si rompiste algo.


8. Publicación en Packagist

Finalmente, para que el mundo use tu creación:

  1. Sube tu código a un repositorio público en GitHub.
  2. Ve a Packagist.org e inicia sesión.
  3. Pega la URL de tu repositorio.
  4. Configura el WebHook en GitHub (Packagist te da las instrucciones) para que cada vez que crees un «Release» o «Tag» en GitHub (ej: v1.0.0), Packagist se actualice automáticamente.

Conclusión

Crear un SDK es un ejercicio de empatía técnica. Pasas de pensar en «¿cómo resuelvo mi problema?» a «¿cómo ayudo a otros a resolver el suyo?».

Al implementar Inyección de Dependencias, DTOs, Testing con Mocks y CI/CD, no solo has creado una librería; has creado un producto robusto. Has dejado de atrapar Rattatas para empezar a diseñar Master Balls.

Ahora, el código es tuyo. ¡A picar código!


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *