Skip to content

Symfony OpenAPI Migration Guide: cyber.trackr.live Implementation ​

This guide is specifically tailored for the cyber.trackr.live team using Symfony and shows how to integrate the reverse-engineered OpenAPI specification into their existing architecture.

Key Learnings from Research ​

Based on our analysis of real Symfony/API Platform projects:

  1. API Platform is the standard - Most modern Symfony APIs use API Platform for OpenAPI support
  2. Configuration is straightforward - Simple YAML configuration enables OpenAPI features
  3. Multiple integration options - Can use API Platform, NelmioApiDocBundle, or custom validation
  4. OpenAPI Factory - API Platform provides sophisticated OpenAPI generation and customization
  5. Real examples show - Projects commonly enable both API Platform and Nelmio for maximum flexibility

Current Understanding ​

Based on our analysis:

  • Symfony PHP framework (confirmed by API owners)
  • OpenAPI 3.1.1 specification reverse-engineered from live API
  • XML XPath queries for DISA STIG data
  • No traditional database - XML files as data source
  • AI integration for mitigation statements

Why This Benefits cyber.trackr.live ​

Your Reverse-Engineered Spec Becomes Official ​

  1. Adopt as source of truth: The spec you created becomes their official API documentation
  2. Validation layer: Ensure API responses match the documented spec
  3. Client generation: Auto-generate clients in multiple languages
  4. Testing foundation: Use spec for contract testing
  5. Future-proofing: Maintain backward compatibility

Integration Approaches for Symfony ​

API Platform is the modern standard for Symfony APIs. While it typically generates OpenAPI from code, you can customize it to work with your existing spec:

yaml
# config/packages/api_platform.yaml
api_platform:
    title: 'Cyber Trackr API'
    description: 'DISA STIGs, SRGs, CCIs, and RMF Controls'
    version: '1.0.3'
    mapping:
        paths: ['%kernel.project_dir%/src/Entity']
    formats:
        json:
            mime_types: ['application/json']
    # Enable Swagger UI
    enable_documentation: true
    # Optional: Also enable NelmioApiDocBundle integration
    enable_nelmio_api_doc: true

To use your existing OpenAPI spec, create a custom OpenAPI factory decorator:

php
<?php
// src/OpenApi/OpenApiFactoryDecorator.php
namespace App\OpenApi;

use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\OpenApi;
use Symfony\Component\Yaml\Yaml;

final class OpenApiFactoryDecorator implements OpenApiFactoryInterface
{
    public function __construct(
        private OpenApiFactoryInterface $decorated,
        private string $projectDir
    ) {}

    public function __invoke(array $context = []): OpenApi
    {
        $openApi = $this->decorated->__invoke($context);
        
        // Load your existing OpenAPI spec
        $customSpec = Yaml::parseFile($this->projectDir . '/openapi/openapi.yaml');
        
        // Merge or replace with your spec as needed
        // This is where you integrate your reverse-engineered spec
        
        return $openApi;
    }
}

Benefits: ​

  • Modern Symfony standard for APIs
  • Can integrate your existing OpenAPI spec
  • Built-in Swagger UI interface
  • Strong ecosystem and community support

Option 2: NelmioApiDocBundle (Traditional) ​

If they're already using Nelmio, they can enhance it with your spec:

yaml
# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
    documentation:
        # Import external OpenAPI spec
        imports:
            - { resource: openapi/openapi.yaml }
    areas:
        path_patterns:
            - ^/api

Option 3: OpenAPI Validation Middleware ​

For minimal changes, add validation to existing controllers using the PSR-7 bridge:

php
<?php
// src/EventListener/OpenApiValidationListener.php
namespace App\EventListener;

use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Psr\Log\LoggerInterface;

class OpenApiValidationListener
{
    private $requestValidator;
    private $responseValidator;
    private $psrHttpFactory;

    public function __construct(
        string $projectDir,
        private LoggerInterface $logger
    ) {
        $validatorBuilder = (new ValidatorBuilder())
            ->fromYamlFile($projectDir . '/openapi/openapi.yaml');
        
        $this->requestValidator = $validatorBuilder->getRequestValidator();
        $this->responseValidator = $validatorBuilder->getResponseValidator();
        
        // PSR-7 bridge for Symfony requests
        $this->psrHttpFactory = new PsrHttpFactory();
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $symfonyRequest = $event->getRequest();
        $psrRequest = $this->psrHttpFactory->createRequest($symfonyRequest);
        
        try {
            $this->requestValidator->validate($psrRequest);
        } catch (ValidationFailed $e) {
            $this->logger->warning('OpenAPI request validation failed', [
                'path' => $symfonyRequest->getPathInfo(),
                'error' => $e->getMessage()
            ]);
            
            // Optional: throw exception to reject invalid requests
            // throw new BadRequestHttpException($e->getMessage());
        }
    }
}

Register as a service:

yaml
# config/services.yaml
services:
    App\EventListener\OpenApiValidationListener:
        arguments:
            $projectDir: '%kernel.project_dir%'
        tags:
            - { name: kernel.event_listener, event: kernel.request, priority: 100 }

Required Packages ​

Install the necessary packages based on your chosen approach:

bash
# For API Platform (Option 1)
composer require api

# For OpenAPI validation (Option 3)
composer require league/openapi-psr7-validator
composer require symfony/psr-http-message-bridge

# For NelmioApiDocBundle (Option 2)
composer require nelmio/api-doc-bundle

Implementation Strategy ​

Phase 1: Documentation Integration (Week 1) ​

Step 1: Add OpenAPI Spec to Project ​

bash
# Add to your Symfony project
cd /path/to/cyber-trackr-symfony
mkdir -p openapi
curl -o openapi/openapi.yaml \
  https://raw.githubusercontent.com/mitre/cyber-trackr-live/main/openapi/openapi.yaml

Step 2: Serve OpenAPI Documentation ​

yaml
# config/routes/api_documentation.yaml
api_docs:
    path: /api/docs
    controller: App\Controller\DocumentationController::index

openapi_spec:
    path: /api/openapi.yaml
    controller: App\Controller\DocumentationController::spec
php
<?php
// src/Controller/DocumentationController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class DocumentationController extends AbstractController
{
    public function index(): Response
    {
        return $this->render('api/documentation.html.twig');
    }

    public function spec(): Response
    {
        $spec = file_get_contents($this->getParameter('kernel.project_dir') . '/openapi/openapi.yaml');
        
        return new Response($spec, 200, [
            'Content-Type' => 'application/x-yaml'
        ]);
    }
}
twig
{# templates/api/documentation.html.twig #}
<!DOCTYPE html>
<html>
<head>
    <title>Cyber Trackr API Documentation</title>
    <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
</head>
<body>
    <div id="swagger-ui"></div>
    <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
    <script>
        SwaggerUIBundle({
            url: '/api/openapi.yaml',
            dom_id: '#swagger-ui',
            presets: [
                SwaggerUIBundle.presets.apis,
                SwaggerUIBundle.presets.standalone
            ]
        });
    </script>
</body>
</html>

Phase 2: Request/Response Validation ​

php
<?php
// src/Entity/Stig.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
    operations: [
        new GetCollection(
            uriTemplate: '/api/stig',
            openapiContext: [
                'summary' => 'List all DISA STIGs',
                'description' => 'Returns all available DISA Security Technical Implementation Guides'
            ]
        ),
        new Get(
            uriTemplate: '/api/stig/{title}/{version}/{release}',
            openapiContext: [
                'summary' => 'Get specific STIG with all requirements',
                'parameters' => [
                    ['name' => 'title', 'in' => 'path', 'required' => true],
                    ['name' => 'version', 'in' => 'path', 'required' => true],
                    ['name' => 'release', 'in' => 'path', 'required' => true]
                ]
            ]
        )
    ]
)]
class Stig
{
    public string $id;
    public string $title;
    public string $version;
    public string $release;
    public array $requirements = [];
}

Using Validation Middleware ​

php
<?php
// src/Service/OpenApiValidator.php
namespace App\Service;

use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

class OpenApiValidator
{
    private $requestValidator;
    private $responseValidator;

    public function __construct(string $specPath)
    {
        $validatorBuilder = (new ValidatorBuilder())
            ->fromYamlFile($specPath);
            
        $this->requestValidator = $validatorBuilder->getServerRequestValidator();
        $this->responseValidator = $validatorBuilder->getResponseValidator();
    }

    public function validateRequest(ServerRequestInterface $request): void
    {
        $this->requestValidator->validate($request);
    }

    public function validateResponse(ResponseInterface $response, ServerRequestInterface $request): void
    {
        $this->responseValidator->validate($response, $request);
    }
}

Phase 3: Existing Business Logic Integration ​

Your existing services remain unchanged:

php
<?php
// src/Service/StigService.php - YOUR EXISTING BUSINESS LOGIC
namespace App\Service;

use DOMDocument;
use DOMXPath;

class StigService
{
    private string $xmlPath;

    public function __construct(string $xmlStoragePath)
    {
        $this->xmlPath = $xmlStoragePath;
    }

    public function getAllDocuments(): array
    {
        // Your existing XML XPath queries remain unchanged
        $documents = [];
        $xmlFiles = glob($this->xmlPath . '/*.xml');
        
        foreach ($xmlFiles as $file) {
            $dom = new DOMDocument();
            $dom->load($file);
            $xpath = new DOMXPath($dom);
            
            // Your existing parsing logic
            $title = $xpath->query('//title')->item(0)->nodeValue;
            $version = $xpath->query('//version')->item(0)->nodeValue;
            // ... continue with existing logic
        }
        
        return $documents;
    }
}

Phase 4: Generate Client Libraries ​

Using your OpenAPI spec, generate official client libraries:

bash
# Generate Ruby client
docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate \
    -i /local/openapi/openapi.yaml \
    -g ruby \
    -o /local/clients/ruby

# Generate Python client
docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate \
    -i /local/openapi/openapi.yaml \
    -g python \
    -o /local/clients/python

# Generate TypeScript client
docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate \
    -i /local/openapi/openapi.yaml \
    -g typescript-fetch \
    -o /local/clients/typescript

Testing with Your OpenAPI Spec ​

Contract Testing ​

php
<?php
// tests/Api/OpenApiContractTest.php
namespace App\Tests\Api;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;

class OpenApiContractTest extends WebTestCase
{
    private $validator;

    protected function setUp(): void
    {
        parent::setUp();
        $this->validator = (new ValidatorBuilder())
            ->fromYamlFile(__DIR__ . '/../../openapi/openapi.yaml')
            ->getResponseValidator();
    }

    public function testStigListMatchesSpec(): void
    {
        $client = static::createClient();
        $client->request('GET', '/api/stig');
        
        $response = $client->getResponse();
        
        // Validate response matches OpenAPI spec
        $this->validator->validate($response, $client->getRequest());
        
        $this->assertResponseIsSuccessful();
    }
}

Advanced Integration Options ​

1. Generate Symfony Code from OpenAPI ​

Use OpenAPI Generator to create Symfony server stubs:

bash
# Generate Symfony server bundle from your spec
docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate \
    -i /local/openapi/openapi.yaml \
    -g php-symfony \
    -o /local/generated-server \
    --additional-properties=bundleName=CyberTrackrApiBundle

2. API Platform with Custom Data Provider ​

Integrate your XML-based data with API Platform:

php
<?php
// src/DataProvider/StigDataProvider.php
namespace App\DataProvider;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Service\StigService;

class StigDataProvider implements ProviderInterface
{
    public function __construct(
        private StigService $stigService
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if ($operation->getUriTemplate() === '/api/stig') {
            return $this->stigService->getAllDocuments();
        }
        
        if (isset($uriVariables['title'])) {
            return $this->stigService->getDocument(
                $uriVariables['title'],
                $uriVariables['version'],
                $uriVariables['release']
            );
        }
        
        return null;
    }
}

3. Symfony Serializer Configuration ​

Ensure responses match your OpenAPI spec format:

yaml
# config/packages/serializer.yaml
framework:
    serializer:
        name_converter: 'serializer.name_converter.camel_case_to_snake_case'
        default_context:
            json_encode_options: JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION

Benefits for cyber.trackr.live Team ​

Immediate Benefits ​

  1. Professional documentation at /api/docs
  2. Your spec becomes official - No more reverse engineering needed
  3. Request/response validation - Catch API drift early
  4. Multi-language clients - Ruby, Python, TypeScript, etc.

Long-term Benefits ​

  1. API governance - Changes require spec updates first
  2. Parallel development - Frontend teams use spec while backend implements
  3. Automated testing - Contract tests ensure compliance
  4. Community adoption - Clear API contract for integrators

Migration Timeline ​

Week 1: Documentation ​

  • [ ] Add OpenAPI spec to repository
  • [ ] Deploy Swagger UI at /api/docs
  • [ ] Share with stakeholders

Week 2: Validation ​

  • [ ] Add request validation middleware
  • [ ] Add response validation in tests
  • [ ] Fix any spec mismatches

Week 3: Client Generation ​

  • [ ] Generate official client libraries
  • [ ] Publish to package repositories
  • [ ] Update documentation

Week 4: Full Integration ​

  • [ ] API Platform or enhanced Nelmio setup
  • [ ] Contract testing suite
  • [ ] CI/CD integration

Key Differences from Laravel Guide ​

  1. Framework-specific: Uses Symfony components and best practices
  2. API Platform focus: Leverages Symfony's leading API framework
  3. Service-based architecture: Aligns with Symfony service patterns
  4. Bundle structure: Can be packaged as reusable Symfony bundle
  5. Symfony testing: Uses WebTestCase and Symfony test tools

Next Steps ​

  1. Review with your team - Does this align with your Symfony setup?
  2. Choose integration approach - API Platform, Nelmio, or custom?
  3. Test locally - Add spec and documentation first
  4. Gradual rollout - Start with docs, add validation, then full integration

This approach lets the cyber.trackr.live team adopt your carefully crafted OpenAPI specification as their official API contract, improving their development process while maintaining their existing Symfony architecture!

Released under the Apache-2.0 License.