Auto-Generate JMS DTOs from API Response Fixtures

๐Ÿ“– 2 minutes read

When integrating with third-party APIs, manually writing DTOs (Data Transfer Objects) for every response structure is tedious and error-prone. Here’s a reusable Laravel command pattern that auto-generates JMS Serializer DTOs from captured API responses.

The Problem

You’re building an integration with an external API that returns complex nested JSON. Writing DTOs by hand means:

  • Manually mapping every field
  • Maintaining JMS annotations
  • Keeping DTOs in sync when the API changes

The Solution: DTO Generator + Fixture Middleware

Step 1: Capture API Responses as Fixtures

Create a Guzzle middleware that saves raw API responses to fixture files during development:

// app/Http/Middleware/FixtureDumperMiddleware.php
namespace App\Http\Middleware;

use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class FixtureDumperMiddleware
{
    public static function create(string $fixtureDir): callable
    {
        return Middleware::tap(
            null,
            function (RequestInterface $request, $options, ResponseInterface $response) use ($fixtureDir) {
                $uri = $request->getUri()->getPath();
                $filename = $fixtureDir . '/' . str_replace('/', '_', trim($uri, '/')) . '.json';
                
                file_put_contents($filename, $response->getBody());
            }
        );
    }
}

Wire it into your HTTP client:

// app/Providers/ApiServiceProvider.php
use GuzzleHttp\HandlerStack;
use App\Http\Middleware\FixtureDumperMiddleware;

public function register()
{
    $this->app->singleton(PaymentClient::class, function ($app) {
        $stack = HandlerStack::create();
        
        if (app()->environment('local')) {
            $stack->push(FixtureDumperMiddleware::create(storage_path('api_fixtures')));
        }
        
        return new PaymentClient([
            'handler' => $stack,
            'base_uri' => config('services.payment.base_url'),
        ]);
    });
}

Step 2: Generate DTOs from Fixtures

Create an artisan command that reads fixture JSON and outputs PHP DTOs:

// app/Console/Commands/GenerateJmsDtosCommand.php
namespace App\Console\Commands;

use Illuminate\Console\Command;

class GenerateJmsDtosCommand extends Command
{
    protected $signature = 'dto:generate {fixture} {--namespace=App\\DTO}';
    protected $description = 'Generate JMS DTOs from API response fixture';

    public function handle()
    {
        $fixturePath = storage_path('api_fixtures/' . $this->argument('fixture'));
        $json = json_decode(file_get_contents($fixturePath), true);
        
        $className = ucfirst(camel_case(basename($fixturePath, '.json')));
        $namespace = $this->option('namespace');
        
        $dto = $this->generateDto($className, $json, $namespace);
        
        $outputPath = app_path('DTO/' . $className . '.php');
        file_put_contents($outputPath, $dto);
        
        $this->info("Generated: {$outputPath}");
    }

    protected function generateDto(string $className, array $data, string $namespace): string
    {
        $properties = [];
        
        foreach ($data as $key => $value) {
            $type = $this->inferType($value);
            $properties[] = sprintf(
                "    /**\n     * @JMS\Type(\"%s\")\n     * @JMS\SerializedName(\"%s\")\n     */\n    public %s $%s;",
                $type,
                $key,
                $this->phpType($type),
                camel_case($key)
            );
        }
        
        return sprintf(
            "inferType($value[0]) . '>' : 'array';
        }
        
        return match (gettype($value)) {
            'integer' => 'int',
            'double' => 'float',
            'boolean' => 'bool',
            default => 'string',
        };
    }

    protected function phpType(string $jmsType): string
    {
        if (str_starts_with($jmsType, 'array')) {
            return 'array';
        }
        return $jmsType;
    }
}

Usage

# 1. Make API calls in local environment (fixtures auto-saved)
php artisan tinker
>>> app(PaymentClient::class)->getTransaction('12345');

# 2. Generate DTO from captured fixture
php artisan dto:generate transaction_response.json --namespace=App\\DTO\\Payment

# Output: app/DTO/Payment/TransactionResponse.php

Benefits

  • Speed: Generate 50+ DTOs in seconds instead of hours
  • Accuracy: No typos or missed fields
  • Maintenance: Re-run when API changes to update DTOs
  • Reusable: Works with any JSON API

Real-World Impact

This pattern was used to generate 40+ DTOs for a payment gateway integration, reducing what would have been 2-3 days of manual work to 15 minutes of automated generation.

The generated DTOs work seamlessly with JMS Serializer for automatic JSON deserialization:

$response = $client->get('/api/transaction/12345');
$transaction = $serializer->deserialize(
    $response->getBody(),
    TransactionResponse::class,
    'json'
);

Keep the generator command in your codebase as a reusable tool for future integrations.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *