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

Each YAML file contains exactly one resource definition. To create a CodeBucket variant, place its schema in a separate module directory named after the variant (for example, StoresApiEU, StoresApiAT) and set the codeBucket: property inside the file. The filename stays the same across all variants — only the parent module directory and the codeBucket: value differ.

>src/Pyz/Glue/StoresApi/resources/api/backend/           # Base variant module
├── stores.resource.yml                                  # No codeBucket property
└── stores.validation.yml

src/Pyz/Glue/StoresApiEU/resources/api/backend/         # EU variant module
├── stores.resource.yml                                  # codeBucket: EU
└── stores.validation.yml                                # codeBucket: EU

src/Pyz/Glue/StoresApiAT/resources/api/backend/         # AT variant module
├── stores.resource.yml                                  # codeBucket: AT
└── stores.validation.yml                                # codeBucket: AT

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/StoresApi/resources/api/backend/stores.resource.yml

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

  provider: "Pyz\\Glue\\StoresApi\\Api\\Backend\\Provider\\StoreBackendProvider"
  processor: "Pyz\\Glue\\StoresApi\\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 the CodeBucket variant in a separate module

Create a new module directory whose name carries the CodeBucket suffix (StoresApiEU) and place the variant’s schema there. The filename stays stores.resource.yml; the codeBucket: property inside the file declares which variant it belongs to.

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

resource:
  name: Stores
  shortName: stores
  description: "EU-specific store resource"
  codeBucket: EU

  # Same provider and processor as the base resource
  provider: "Pyz\\Glue\\StoresApi\\Api\\Backend\\Provider\\StoreBackendProvider"
  processor: "Pyz\\Glue\\StoresApi\\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"
One resource per file

Each schema file must contain exactly one top-level resource: block. CodeBucket variants live in their own files in their own module directories — they cannot be stacked inside a single YAML document.

Step 3: Create the variant’s validation schema

Place the matching validation file alongside the variant resource, in the same module directory. Set codeBucket: at the root so the parser pairs it with the EU resource variant.

src/Pyz/Glue/StoresApiEU/resources/api/backend/stores.validation.yml

codeBucket: EU

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/StoresApi/Api/Backend/Provider/StoreBackendProvider.php

<?php

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

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Pyz\Glue\StoresApi\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 variants are a project-level concept only — they never exist in the core (vendor) or feature layers. Core and feature layers contribute properties to the base resource. The project-level CodeBucket variant then inherits from that already-merged base and adds its own properties on top.

Where CodeBucket files live

The codeBucket: property is only ever set in files under src/Pyz/Glue/{Module}{CodeBucketName}/.... A schema file in vendor/spryker/... or src/SprykerFeature/... must not declare a codeBucket: — those layers always describe the base resource.

Example: base resource built across layers + project-level EU variant

The base Stores resource is assembled from the core, feature, and project layers in that precedence order. The EU variant lives only in the project layer, in its own variant module.

Core layer (vendor) — base only, no codeBucket:

# vendor/spryker/store/resources/api/backend/stores.resource.yml
resource:
  name: Stores
  shortName: stores
  properties:
    name:
      type: string
    timezone:
      type: string

Feature layer — base only, no codeBucket:

# src/SprykerFeature/CRM/resources/api/backend/stores.resource.yml
resource:
  name: Stores
  properties:
    countries:
      type: array

Project base layer — base only, no codeBucket:

# src/Pyz/Glue/StoresApi/resources/api/backend/stores.resource.yml
resource:
  name: Stores
  properties:
    timezone:
      required: true      # tighten the core definition

Project CodeBucket variant — only this file carries codeBucket::

# src/Pyz/Glue/StoresApiEU/resources/api/backend/stores.resource.yml
resource:
  name: Stores
  codeBucket: EU
  properties:
    taxRate:
      type: number
      required: true      # EU-specific addition
    companyVatId:
      type: string        # EU-specific addition

Merged result (StoresEUBackendResource) — base properties inherited from the core → feature → project merge, EU-specific properties added on top:

resource:
  name: Stores
  codeBucket: EU
  properties:
    name:                 # from core
      type: string
    timezone:             # from core, tightened by project base
      type: string
      required: true
    countries:            # from feature
      type: array
    taxRate:              # from project EU variant
      type: number
      required: true
    companyVatId:         # from project EU variant
      type: string

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
# src/Pyz/Glue/StoresApi/resources/api/backend/stores.resource.yml
resource:
  name: Stores
  properties:
    idStore:
      type: integer
    name:
      type: string
    timezone:
      type: string
# ✅ EU variant lives in its own module directory
# src/Pyz/Glue/StoresApiEU/resources/api/backend/stores.resource.yml
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\\StoresApi\\Api\\Backend\\Provider\\StoreBackendProvider"
  processor: "Pyz\\Glue\\StoresApi\\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