CodeBucket Support in API Platform

Edit on GitHub

This document explains how to create and use CodeBucket-specific API resources in API Platform.

Overview

CodeBucket support enables API Platform to serve different resource variants based on the runtime APPLICATION_CODE_BUCKET environment constant. This allows you to maintain Code Bucket-specific API resources without requiring separate container compilations for each Code Bucket.

Use cases

Use CodeBucket resources when you need:

  • Code Bucket-specific properties: EU-specific GDPR fields, tax rates, compliance data
  • Code Bucket-specific validation: Different validation rules per Code Bucket or country
  • Localized business logic: Country-specific processing requirements

Key benefits

  • Single container: Compile once, serve hundreds of CodeBuckets
  • Runtime resolution: Automatic selection of correct resource variant
  • Graceful fallback: Base resources used when CodeBucket variant doesn’t exist
  • No duplication: Share common logic while extending for specific needs

How CodeBucket resolution works

Architecture overview

API Platform uses a decorator pattern to intercept resource resolution and select the appropriate CodeBucket variant at runtime:

>Request → Symfony Routing → API Platform
                              ↓
                    CodeBucket Decorators
                              ↓
           ┌──────────────────┴──────────────────┐
           │                                     │
    Resource Class Resolver         Resource Name Collection Factory
    (Runtime resolution)            (Compile-time filtering)
           │                                     │
           └──────────────────┬──────────────────┘
                              ↓
                    Selected Resource Variant
                    (Base or CodeBucket-specific)

Runtime resolution flow

>1. Request: GET glue-backend.eu.spryker.local/stores
   APPLICATION_CODE_BUCKET = 'EU'

2. CodeBucketResourceNameCollectionFactory
   → Filters resource collection at compile time
   → Includes only EU variants or base resources (no conflicts)

3. CodeBucketResourceClassResolver
   → Reads APPLICATION_CODE_BUCKET = 'EU'
   → Base class: StoresBackendResource
   → Builds variant name: StoresEUBackendResource
   → Checks: class_exists('Generated\Api\Backend\StoresEUBackendResource')
   → Validates: StoresEUBackendResource::CODE_BUCKET === 'EU'
   → Returns: StoresEUBackendResource

4. API Platform executes with EU-specific resource
   → Properties: base + EU-specific fields
   → Validations: base + EU-specific rules

CodeBucket detection mechanism

The system identifies CodeBucket resources using three checks:

  1. Class existence: class_exists($codeBucketClassName)
  2. Constant check: defined("$className::CODE_BUCKET")
  3. Value match: constant("$className::CODE_BUCKET") === $currentCodeBucket

Generated CODE_BUCKET constant

CodeBucket resources automatically get a CODE_BUCKET constant during generation:

// StoresEUBackendResource.php (EU variant)
final class StoresEUBackendResource
{
    public const string CODE_BUCKET = 'EU';

    // Properties...
}

// StoresBackendResource.php (base - no constant)
final class StoresBackendResource
{
    // Properties... (no CODE_BUCKET constant)
}

Resource naming conventions

File naming pattern

Resource schemas follow the pattern: {resource-name}.resource.yml Validation schemas follow the pattern: {resource-name}.validation.yml

CodeBuckets are specified inside the schema files, not in the filename.

>src/Pyz/Glue/Store/resources/api/backend/
├── stores.resource.yml              # Resource schema (CodeBucket variants defined inside)
└── stores.validation.yml            # Validation schema (CodeBucket variants defined inside)

Generated class naming pattern

The generator creates classes following: {ResourceName}{CodeBucket}{ApiType}Resource

Schema File CodeBucket (defined in file) Generated Class
stores.resource.yml None (base) StoresBackendResource
stores.resource.yml EU StoresEUBackendResource
stores.resource.yml AT StoresATBackendResource
stores.resource.yml (Storefront) EU StoresEUStorefrontResource

URL consistency

All CodeBucket variants share the same URL path, with the Code Bucket defined in the domain:

  • EU: glue-backend.eu.spryker.local/storesStoresEUBackendResource
  • AT: glue-backend.at.spryker.local/storesStoresATBackendResource
  • DE: glue-backend.de.spryker.local/storesStoresBackendResource (or StoresDEBackendResource if variant exists)

The URL path is identical (/stores), but the Code Bucket in the domain determines which resource variant is used. Only the properties, validations, and business logic differ between variants.

Creating CodeBucket resources

Basic example

Step 1: Create base resource

src/Pyz/Glue/Store/resources/api/backend/stores.resource.yml

resource:
  name: Stores
  shortName: Store
  description: "Store resource"

  provider: "Pyz\\Glue\\Store\\Api\\Backend\\Provider\\StoreBackendProvider"
  processor: "Pyz\\Glue\\Store\\Api\\Backend\\Processor\\StoreBackendProcessor"

  operations:
    - type: Get
    - type: GetCollection

  properties:
    idStore:
      type: integer
      writable: false
      identifier: true

    name:
      type: string
      description: "Store name"

    timezone:
      type: string
      description: "Store timezone"

Step 2: Define CodeBucket variant

Within the same stores.resource.yml file, define the EU-specific variant by specifying the CodeBucket:

# EU-specific store resource (defined in same file)
resource:
  name: Stores
  shortName: Store
  description: "EU-specific store resource"
  codeBucket: EU

  # Same provider and processor as base
  provider: "Pyz\\Glue\\Store\\Api\\Backend\\Provider\\StoreBackendProvider"
  processor: "Pyz\\Glue\\Store\\Api\\Backend\\Processor\\StoreBackendProcessor"

  operations:
    - type: Get
    - type: GetCollection

  properties:
    idStore:
      type: integer
      writable: false
      identifier: true

    name:
      type: string
      description: "Store name"

    timezone:
      type: string
      description: "Store timezone"

    # EU-specific properties
    taxRate:
      type: number
      description: "EU tax rate"
      openapiContext:
        example: 19.0

    gdprContactEmail:
      type: string
      description: "GDPR contact email"
      openapiContext:
        example: "[email protected]"

    vatRegistrationNumber:
      type: string
      description: "VAT registration number"

Step 3: Create validation schemas

Within src/Pyz/Glue/Store/resources/api/backend/stores.validation.yml, define CodeBucket-specific validation:

post:
  taxRate:
    - NotBlank
    - Range:
        min: 0
        max: 100

  gdprContactEmail:
    - NotBlank
    - Email

  vatRegistrationNumber:
    - NotBlank
    - Regex:
        pattern: '/^[A-Z]{2}[0-9]{8,12}$/'
        message: "Invalid VAT format"

Step 4: Generate resources

docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate backend

This generates:

  • src/Generated/Api/Backend/StoresBackendResource.php (base)
  • src/Generated/Api/Backend/StoresEUBackendResource.php (with CODE_BUCKET = 'EU')

Step 5: Implement Provider with CodeBucket awareness

src/Pyz/Glue/Store/Api/Backend/Provider/StoreBackendProvider.php

<?php

namespace Pyz\Glue\Store\Api\Backend\Provider;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Pyz\Glue\Store\Business\StoreFacadeInterface;

class StoreBackendProvider implements ProviderInterface
{
    public function __construct(
        protected StoreFacadeInterface $storeFacade,
    ) {
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if (isset($uriVariables['idStore'])) {
            return $this->getStore((int) $uriVariables['idStore'], $operation);
        }

        return $this->getStores($operation);
    }

    protected function getStore(int $idStore, Operation $operation): ?object
    {
        $storeTransfer = $this->storeFacade->findStoreById($idStore);

        if ($storeTransfer === null) {
            return null;
        }

        $resourceClass = $operation->getClass();
        $resource = new $resourceClass();
        $resource->fromArray($storeTransfer->toArray());

        // Add EU-specific data if this is an EU resource
        if (defined("$resourceClass::CODE_BUCKET") && $resourceClass::CODE_BUCKET === 'EU') {
            $resource->taxRate = $storeTransfer->getTaxRate();
            $resource->gdprContactEmail = $storeTransfer->getGdprContactEmail();
            $resource->vatRegistrationNumber = $storeTransfer->getVatRegistrationNumber();
        }

        return $resource;
    }

    protected function getStores(Operation $operation): array
    {
        $stores = $this->storeFacade->getAllStores();

        $resources = [];
        foreach ($stores as $storeTransfer) {
            $resourceClass = $operation->getClass();
            $resource = new $resourceClass();
            $resource->fromArray($storeTransfer->toArray());

            if (defined("$resourceClass::CODE_BUCKET") && $resourceClass::CODE_BUCKET === 'EU') {
                $resource->taxRate = $storeTransfer->getTaxRate();
                $resource->gdprContactEmail = $storeTransfer->getGdprContactEmail();
                $resource->vatRegistrationNumber = $storeTransfer->getVatRegistrationNumber();
            }

            $resources[] = $resource;
        }

        return $resources;
    }
}

Schema layering with CodeBuckets

CodeBucket resources support multi-layer schema merging just like base resources.

Example: Multi-layer CodeBucket resource

Core layer (vendor):

# vendor/spryker/store/resources/api/backend/stores.resource.yml
resource:
  name: Stores
  codeBucket: EU
  properties:
    taxRate:
      type: number

Project layer:

# src/Pyz/Glue/Store/resources/api/backend/stores.resource.yml
resource:
  name: Stores
  codeBucket: EU
  properties:
    taxRate:
      required: true  # Override core
    companyVatId:
      type: string    # Project-specific

Merged result (StoresEUBackendResource):

resource:
  name: Stores
  properties:
    taxRate:
      type: number
      required: true      # From project
    companyVatId:
      type: string        # From project

Fallback behavior

Graceful degradation

When APPLICATION_CODE_BUCKET is set but no matching variant exists, the system falls back to the base resource:

>Scenario: APPLICATION_CODE_BUCKET = 'AT'
Available resources:
  - StoresBackendResource (base)
  - StoresEUBackendResource (EU variant)

Result: StoresBackendResource is used (graceful fallback)
No errors, system continues to work

No CodeBucket set

When APPLICATION_CODE_BUCKET is not set:

>Available resources:
  - StoresBackendResource (base)
  - StoresEUBackendResource (EU variant)
  - StoresATBackendResource (AT variant)

Result: StoresBackendResource is used
All CodeBucket variants are excluded from resource collection

Resource collection filtering

CodeBucketResourceNameCollectionFactory

This decorator filters the resource collection at compile time to prevent conflicts between base and CodeBucket variants.

When APPLICATION_CODE_BUCKET is not set:

  • Include all base resources (no CODE_BUCKET constant)
  • Exclude all CodeBucket variants

When APPLICATION_CODE_BUCKET is set (example: ‘EU’):

  1. Group resources by base class name
  2. For each group:
    • If EU variant exists → include ONLY the EU variant
    • If no EU variant exists → include base resource (fallback)
    • Exclude all non-matching CodeBucket variants

Example filtering:

>Available classes:
  - StoresBackendResource (base)
  - StoresEUBackendResource (EU)
  - StoresATBackendResource (AT)
  - CustomersBackendResource (base, no variants)

With APPLICATION_CODE_BUCKET = 'EU':
  ✓ StoresEUBackendResource (EU variant exists)
  ✗ StoresBackendResource (excluded, EU variant preferred)
  ✗ StoresATBackendResource (excluded, wrong CodeBucket)
  ✓ CustomersBackendResource (no variant, base used)

With APPLICATION_CODE_BUCKET = 'DE':
  ✓ StoresBackendResource (no DE variant, fallback to base)
  ✗ StoresEUBackendResource (excluded, wrong CodeBucket)
  ✗ StoresATBackendResource (excluded, wrong CodeBucket)
  ✓ CustomersBackendResource (no variant, base used)

Benefits of filtering

  • OpenAPI Generation: Only the correct variant appears in OpenAPI documentation
  • Route Registration: Only the correct variant routes are registered
  • No Conflicts: Base and CodeBucket variants never coexist in routing
  • Performance: Filtering happens once and is cached

Container compilation

Single container for all CodeBuckets

API Platform compiles a single container that contains ALL resources (base and all CodeBucket variants). Runtime resolution selects the correct variant per request.

At compile time:

  1. Discover all resource classes in src/Generated/Api/{ApiType}/
  2. Build metadata for ALL resources (base + all CodeBucket variants)
  3. Register routes for ALL resources (all map to same URLs)
  4. Cache everything

At runtime:

  1. CodeBucket decorators intercept resolution
  2. Select correct resource based on APPLICATION_CODE_BUCKET
  3. Only selected resource is used for that request
  4. Different CodeBuckets served from same container

Performance characteristics

  • One-time cost: Container compilation happens once
  • No overhead: Runtime resolution uses local cache per request
  • Scalability: Supports hundreds of CodeBuckets without performance impact

Debugging CodeBucket resources

Verify CODE_BUCKET constant generation

After regenerating resources, verify the constant was added:

# Check EU resource has constant
grep "CODE_BUCKET" src/Generated/Api/Backend/StoresEUBackendResource.php
# Expected: public const string CODE_BUCKET = 'EU';

# Verify base resource has no constant
grep "CODE_BUCKET" src/Generated/Api/Backend/StoresBackendResource.php
# Expected: no match (empty output)

Debug resource resolution

Use the debug command to inspect which resources are available:

# List all resources
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug --list

# Show specific resource
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug stores --api-type=backend

# Show merged schema
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug stores --api-type=backend --show-merged

# Show all contributing source files
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug stores --api-type=backend --show-sources

Verify runtime resolution

Test different CodeBuckets by accessing different domains:

# Test EU CodeBucket
curl -X GET http://glue-backend.eu.spryker.local/stores
# Should include: taxRate, gdprContactEmail, vatRegistrationNumber

# Test AT CodeBucket (if no AT variant exists)
curl -X GET http://glue-backend.at.spryker.local/stores
# Should return base resource (fallback)

# Test DE CodeBucket
curl -X GET http://glue-backend.de.spryker.local/stores
# Should return base resource or DE-specific variant if available

Best practices

1. Keep base resources complete

The base resource should contain all common properties that work across all CodeBuckets:

# ✅ Good - Base is complete
# stores.resource.yml (base variant)
resource:
  name: Stores
  properties:
    idStore:
      type: integer
    name:
      type: string
    timezone:
      type: string

# stores.resource.yml (EU variant - defined in same file)
resource:
  name: Stores
  codeBucket: EU
  properties:
    idStore:
      type: integer
    name:
      type: string
    timezone:
      type: string
    taxRate:        # EU-specific addition
      type: number

2. Use CodeBuckets for true regional differences

Only create CodeBucket variants when there are genuine regional requirements:

# ✅ Good use cases:
- EU GDPR compliance fields
- Code Bucket-specific tax calculations
- Country-specific validation rules
- Code Bucket-specific business logic

# ❌ Bad use cases:q
- Language translations (use locales instead)
- Minor field variations (use base resource)
- Temporary feature flags (use feature toggles)

3. Share Provider and Processor logic

Use the same Provider and Processor classes for base and CodeBucket variants when possible:

# Both use same implementation
resource:
  provider: "Pyz\\Glue\\Store\\Api\\Backend\\Provider\\StoreBackendProvider"
  processor: "Pyz\\Glue\\Store\\Api\\Backend\\Processor\\StoreBackendProcessor"

Detect the resource type inside the Provider using the CODE_BUCKET constant:

$resourceClass = $operation->getClass();
if (defined("$resourceClass::CODE_BUCKET")) {
    $codeBucket = $resourceClass::CODE_BUCKET;
    // Apply CodeBucket-specific logic
}

4. Document CodeBucket-specific fields

Clearly document which fields are CodeBucket-specific in your schema descriptions:

gdprContactEmail:
  type: string
  description: "GDPR contact email (EU-specific requirement for GDPR compliance)"

5. Test fallback behavior

Always test that your application works correctly when:

  • CodeBucket is not set (uses base resource)
  • CodeBucket is set but variant doesn’t exist (falls back to base)
  • CodeBucket variant exists (uses variant)

Testing

CodeBucket resources can be tested using the standard API Platform testing infrastructure. The test environment automatically generates resources before tests execute.

For detailed testing guidance, see API Platform Testing.

Troubleshooting

Resource not found for CodeBucket

If you get a “Resource not found” error for a specific CodeBucket:

  1. Verify the schema file naming: {resource-name}.resource.yml for resources, {resource-name}.validation.yml for validations
  2. Verify the CodeBucket is defined inside the schema file
  3. Regenerate resources: docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate backend
  4. Verify the CODE_BUCKET constant exists in the generated class
  5. Check that APPLICATION_CODE_BUCKET matches the CodeBucket defined in your schema file

Wrong resource variant used

If the wrong variant is being used:

  1. Verify APPLICATION_CODE_BUCKET is set correctly in your environment
  2. Clear caches: docker/sdk cli rm -rf data/cache/*
  3. Check that the resource class name follows the pattern: {Name}{CodeBucket}{ApiType}Resource
  4. Verify the CODE_BUCKET constant value matches APPLICATION_CODE_BUCKET

For more troubleshooting guidance, see API Platform Troubleshooting.

Next steps