<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Spryker Documentation</title>
        <description>Spryker documentation center.</description>
        <link>https://docs.spryker.com/</link>
        <atom:link href="https://docs.spryker.com/feed.xml" rel="self" type="application/rss+xml"/>
        <lastBuildDate>Wed, 12 Aug 2026 15:42:10 +0000</lastBuildDate>
        <generator>Jekyll v4.2.2</generator>
        
        
        <item>
            <title>Security release notes 202608.0</title>
            <description>This document describes the security-related issues that have been recently resolved.

For additional support with this content, [contact our support](https://support.spryker.com/). If you found a new security vulnerability, contact us at [security@spryker.com](mailto:security@spryker.com).

## Removal of eval() function

Use of the eval() function has been removed from the codebase. Even though no security issues were identified due to its use, it was removed in order to follow security best practices.

### Affected modules

- `spryker/testify`: &lt; 3.66.0

### Fix the vulnerability

Update the affected Spryker package:

```bash
composer update spryker/testify:&quot;^3.66.0&quot;
composer show spryker/testify # Verify the version
```

Add or adjust the $config[TestifyConstants::IS_DATA_BUILDER_RULE_EVAL_ENABLED] line within the `config/Shared/config_default.php` file:

```bash
use Spryker\Shared\Testify\TestifyConstants;

if (class_exists(TestifyConstants::class)) {
    $config[TestifyConstants::IS_DATA_BUILDER_RULE_EVAL_ENABLED] = false;
}
```

## Vulnerabilities in third-party dependencies

Several third-party dependencies were updated to address publicly known vulnerabilities present in earlier versions. The updated dependencies are listed below.

### Affected packages

- `symfony/twig-bridge`: &lt; 6.4.43
- `nikic/php-parser` : &lt; 5.8.0
- `aws/aws-sdk-php` : &lt; 3.389.3
- `symfony/security-core` : &lt; 6.4.43

### Fix the vulnerability

```bash
composer update symfony/twig-bridge nikic/php-parser aws/aws-sdk-php symfony/security-core
```
</description>
            <pubDate>Wed, 12 Aug 2026 15:37:31 +0000</pubDate>
            <link>https://docs.spryker.com/docs/about/all/releases/security-releases/security-release-notes-202608.0.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/about/all/releases/security-releases/security-release-notes-202608.0.html</guid>
            
            
        </item>
        
        <item>
            <title>Workflows feature overview</title>
            <description>The Workflows feature lets Back Office users design and operate state machines directly from the Back Office. A workflow describes how a subject moves through a sequence of states as events occur, conditions are met, or timeouts elapse.

A subject can be anything — a company, a user, a product, or a custom entity. A workflow starts when its subject reaches a specific application event, defined in a trigger: for example a company being created, a user registering, or a merchant being updated. From then on, each subject runs through the workflow independently.

Unlike classic state machines, which are defined in XML files and require a deployment to change, a workflow is created and adjusted by a Back Office user. This puts process design in the hands of the people who own the process.

## An example

Consider onboarding a new B2B company. When a company is created, a workflow starts and walks it through a series of states: *business verification*, then *contract agreement*, then *customer group assignment*, and finally *approved*. Some steps advance on their own once a condition is met (for example, the business has been verified); others wait for a Back Office user to confirm; and a step can time out if nothing happens. The whole process — its states, the order of the steps, and the rules between them — is defined and adjusted in the Back Office, not in code.

## How a workflow works

The clearest way to understand the feature is to follow one small workflow from start to finish. The example below is a company-onboarding process: a new company moves from `created` to `approved`, passing a verification check and an automatic waiting step along the way.

A workflow definition is an XML document in the `state-machine-01` format. It has three parts: `states`, `transitions`, and `events`:

```xml
{% raw %}&lt;statemachine xmlns=&quot;spryker:state-machine-01&quot;&gt;
    &lt;process name=&quot;CompanyOnboarding&quot; main=&quot;true&quot;&gt;

        &lt;states&gt;
            &lt;state name=&quot;created&quot;/&gt;
            &lt;state name=&quot;business verification&quot;/&gt;
            &lt;state name=&quot;approved&quot;/&gt;
            &lt;state name=&quot;denied&quot;/&gt;
        &lt;/states&gt;

        &lt;transitions&gt;
            &lt;!-- A plain event transition: the &quot;initiate&quot; event moves the subject forward. --&gt;
            &lt;transition happy=&quot;true&quot;&gt;
                &lt;source&gt;created&lt;/source&gt;
                &lt;target&gt;business verification&lt;/target&gt;
                &lt;event&gt;initiate&lt;/event&gt;
            &lt;/transition&gt;

            &lt;!-- A guarded transition: it fires only if the condition returns true. --&gt;
            &lt;transition happy=&quot;true&quot; condition=&quot;CompanyOnboarding/IsBusinessVerified&quot;&gt;
                &lt;source&gt;business verification&lt;/source&gt;
                &lt;target&gt;approved&lt;/target&gt;
                &lt;event&gt;verify business&lt;/event&gt;
            &lt;/transition&gt;

            &lt;!-- The same event with no condition sends the subject the other way. --&gt;
            &lt;transition&gt;
                &lt;source&gt;business verification&lt;/source&gt;
                &lt;target&gt;denied&lt;/target&gt;
                &lt;event&gt;verify business&lt;/event&gt;
            &lt;/transition&gt;
        &lt;/transitions&gt;

        &lt;events&gt;
            &lt;event name=&quot;initiate&quot; onEnter=&quot;true&quot;/&gt;
            &lt;event name=&quot;verify business&quot; timeout=&quot;3 second&quot;/&gt;
        &lt;/events&gt;

    &lt;/process&gt;
&lt;/statemachine&gt;{% endraw %}
```

Here is how a company travels through this definition:

1. **The instance starts.** A trigger (see [Triggers](#triggers)) starts an instance for a new company. The instance begins in the initial state you set when you author the version — here, `created`.
2. **An `onEnter` event fires automatically.** The `initiate` event is marked `onEnter=&quot;true&quot;`, so the engine fires it as soon as the instance is in `created`, moving the company to `business verification`.
3. **A timeout waits, then a condition decides.** The `verify business` event has a `timeout=&quot;3 second&quot;`. After the timeout elapses, the engine evaluates the two `verify business` transitions in order. The first is guarded by the `CompanyOnboarding/IsBusinessVerified` condition: if it returns `true`, the company moves to `approved`. If not, the unguarded transition sends it to `denied`.

Three ideas in that trace do the heavy lifting, and each maps to a piece of project code a developer can plug in:

| In the definition | What it means | You provide |
|-------------------|---------------|-------------|
| `event` | A named step. It can fire automatically (`onEnter`), after a delay (`timeout`), on a user action (`manual`), or from application code. | Nothing — events are declared in the XML. |
| `condition=&quot;…&quot;` | A guard that lets a transition fire only when a business rule is true. | A [condition plugin](#conditions). |
| `command=&quot;…&quot;` | Project logic to run during a transition (not shown above — for example, &quot;mark the company approved&quot;). | A [command plugin](#commands). |

## Core concepts

A workflow is built from four entities. The example above is a *version* of a *process*; running it for one company creates an *instance*; and what starts that instance is a *trigger*. You manage all of them in the Back Office under **Administration &gt; Workflows**.

| Concept | What it is |
|---------|------------|
| Process | The workflow itself: a named process bound to a subject type (for example `Company`). It is created once and never replaced — all versions and instances belong to it. |
| Version | One complete definition of the process at a point in time (the XML above): its states, transitions, and events, plus the initial state. A process can have many versions, but only one is *active* at a time. |
| Trigger | Connects an application event (for example &quot;a company was created&quot;) to the process, so that event automatically starts a new instance. |
| Instance | One subject running through the workflow — for example one specific company being onboarded. It tracks the subject&apos;s current state. |

### Why versions matter

Every instance is pinned to the version it started on. When you activate a newer version, running instances do **not** jump to it — they finish on the version they began with. Only *new* instances start on the newly active version.

This is what makes editing a live process safe. You publish an improved version for future subjects, while in-flight subjects complete on the exact rules they started with — no half-migrated instances and no transitions that suddenly point at states that no longer exist.

## Subjects

A workflow is attached to a *subject type*, which you choose when you create the process. It is not limited to companies: the subject type is a label you define — `Company`, `Customer`, `Product`, or anything your project needs. The trigger you configure decides which application event starts an instance for that subject.

For example, a process with subject type `Company` and a trigger on company creation starts one instance per created company, and each company then runs its own instance independently.

## Instances

An *instance* is a single subject running through a workflow — for example one specific company being onboarded. Each instance is pinned to the version it started on and tracks its current state. In the Back Office you can inspect instances, see their current state, and trigger any manual actions the workflow defines (for example, a step that a Back Office user must confirm before the workflow continues).

{% info_block infoBox &quot;Instance history retention&quot; %}

Each instance and its transition history are kept in the database. This MVP does not ship an automated cleanup or retention job, so plan for periodic housekeeping if you expect a high volume of instances.

{% endinfo_block %}

## Transitions

A workflow advances through three kinds of transitions:

- **Event transitions**: triggered by an application event or a manual action in the Back Office.
- **Condition transitions**: advance automatically once a business condition becomes true.
- **Timeout transitions**: advance automatically after a defined period elapses.

Condition and timeout transitions have no incoming event to push them, so two console commands advance them on a schedule:

- `workflow:check-condition` — advances every condition transition whose condition has become `true`.
- `workflow:check-timeout` — advances every timeout transition whose timeout has elapsed.

Without them, an instance that reaches a condition or timeout transition waits forever. These commands are **not** scheduled out of the box; the [installation guide](/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html) shows how to register them as recurring jobs so workflows progress on their own.

## Triggers

A *trigger* connects an application event to a workflow so that a new instance starts automatically. For example, creating a company can trigger a new onboarding instance. You select the trigger event in the Back Office when you configure the process; the available events are provided by trigger plugins (see [Extending a workflow](#extending-a-workflow)).

## Extending a workflow

The definition references project code by name in two places: `condition=&quot;…&quot;` and `command=&quot;…&quot;`. A developer implements each as a plugin and registers it in the project `WorkflowDependencyProvider` (see the [installation guide](/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html)). The engine matches a plugin to a definition by two values: its `getName()` (the string used in the XML) and its `getSubjectType()` (so the same name can behave differently for different subjects).

### Commands

A *command* runs project logic during a transition — for example, marking a company approved when it enters the `approved` state. Reference it in the definition as `command=&quot;CompanyOnboarding/MarkCompanyActiveAndApproved&quot;`, then implement `WorkflowCommandPluginInterface` so `getName()` returns that same string:

```php
interface WorkflowCommandPluginInterface extends CommandPluginInterface
{
    public function getName(): string;

    public function getSubjectType(): string;
}
```

### Conditions

A *condition* guards a transition: the workflow takes that transition only when the condition returns `true` — for example, &quot;the business is verified.&quot; Reference it as `condition=&quot;CompanyOnboarding/IsBusinessVerified&quot;`, then implement `WorkflowConditionPluginInterface`:

```php
interface WorkflowConditionPluginInterface extends ConditionPluginInterface
{
    public function getName(): string;

    public function getSubjectType(): string;
}
```

### Trigger plugins

A trigger plugin provides an application event that can start an instance. Unlike commands and conditions — which are named inside the definition XML — a trigger is chosen in the Back Office when you configure the process. A trigger plugin binds an application event (`getEventName()`, for example `Entity.spy_company.create`) to a subject type, so that whenever that event fires for that subject, a new instance starts. Implement `StateMachineProcessTriggerPluginInterface`:

```php
interface StateMachineProcessTriggerPluginInterface
{
    public function getEventName(): string;

    public function getName(): string;

    public function getSubjectType(): string;

    public function getDescription(): string;
}
```

## Provisioning workflows on installation

Workflows can be shipped with a project and provisioned automatically during installation through data import. This means a demo or production workflow is available immediately after setup, and re-importing the same workflow does not create duplicates. See the [installation guide](/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html) for details.
</description>
            <pubDate>Tue, 11 Aug 2026 14:34:40 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/back-office/latest/base-shop/workflow-feature-overview.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/back-office/latest/base-shop/workflow-feature-overview.html</guid>
            
            
        </item>
        
        <item>
            <title>Install the Workflows feature</title>
            <description>This document describes how to install the Workflow feature.

## Prerequisites

Install the required modules:

| MODULE | VERSION |
|--------|---------|
| spryker/workflow | ^1.0.0 |
| spryker/state-machine | ^2.26.0 |
| spryker/data-import | ^1.27.1 |
| spryker/gui | ^5.3.2 |
| spryker/kernel | ^3.84.0 |

## Install feature core

### 1) Install the required modules

Install the Workflow module and update its dependencies to the required versions:

```bash
composer require spryker/workflow:&quot;^1.0.0&quot; --update-with-dependencies
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure the following modules are available in `vendor/spryker/`:

| MODULE |
|--------|
| spryker/workflow |
| spryker/state-machine |
| spryker/data-import |
| spryker/gui |

{% endinfo_block %}

### 2) Set up the database schema and transfer objects

Apply the database changes and generate the transfer objects:

```bash
docker/sdk cli console propel:install
docker/sdk cli console transfer:generate
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure that the following tables exist in the database:

| DATABASE ENTITY | TYPE | EVENT |
|-----------------|------|-------|
| spy_state_machine_process | table | created |
| spy_state_machine_process_definition | table | created |
| spy_state_machine_process_definition_instance | table | created |
| spy_state_machine_process_definition_trigger | table | created |

{% endinfo_block %}

### 3) Configure navigation

The Workflow Back Office menu entry is added under **Administration**. Add the following node to `config/Zed/navigation.xml`:

```xml
&lt;workflows&gt;
    &lt;label&gt;Workflows&lt;/label&gt;
    &lt;title&gt;Workflows&lt;/title&gt;
    &lt;bundle&gt;workflow&lt;/bundle&gt;
    &lt;controller&gt;process&lt;/controller&gt;
    &lt;action&gt;index&lt;/action&gt;
&lt;/workflows&gt;
```

Rebuild the navigation cache:

```bash
docker/sdk cli console navigation:build-cache
docker/sdk cli console cache:empty-all
```

{% info_block warningBox &quot;Verification&quot; %}

In the Back Office, go to **Administration &gt; Workflows** and make sure the page opens.

{% endinfo_block %}

### 4) Create a workflow in the Back Office

Once the feature is installed, a Back Office user can author a workflow through the UI:

1. Go to **Administration &gt; Workflows** and create a process. Give it a name and a subject type (for example `Company`).
2. Open the process&apos;s **Workflow Versions**, then **Create Version**: paste the definition XML (in the `state-machine-01` format) and set the initial state, which is the process&apos;s entry point. Save the version.
3. **Activate** the version.
4. Open the process&apos;s **Workflow Triggers**. The page lists the trigger events registered for the process&apos;s subject type (&quot;Select events that start this workflow&quot;). Select one or more and **Save Triggers**.
5. **Activate** the process.

The workflow is now live: whenever a selected trigger event fires for a subject of the configured type, a new instance starts on the active version.

{% info_block infoBox &quot;Provisioning instead of manual authoring&quot; %}

To ship a ready-made workflow with your project instead of creating it by hand, use the data import in the following steps. The two approaches are interchangeable — both produce the same process, versions, and triggers.

{% endinfo_block %}

### 5) Register command, condition, and trigger plugins

Register your project&apos;s commands, conditions, and start triggers by extending the core `WorkflowDependencyProvider`.

**src/Pyz/Zed/Workflow/WorkflowDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Zed\Workflow;

use Spryker\Zed\Workflow\WorkflowDependencyProvider as SprykerWorkflowDependencyProvider;

class WorkflowDependencyProvider extends SprykerWorkflowDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Zed\Workflow\Dependency\Plugin\WorkflowCommandPluginInterface&gt;
     */
    protected function getCommandPlugins(): array
    {
        return [
            // new MyCommandPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\Workflow\Dependency\Plugin\WorkflowConditionPluginInterface&gt;
     */
    protected function getConditionPlugins(): array
    {
        return [
            // new MyConditionPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\Workflow\Dependency\Plugin\StateMachineProcessTriggerPluginInterface&gt;
     */
    protected function getTriggerPlugins(): array
    {
        return [
            // new MyProcessTriggerPlugin(),
        ];
    }
}
```

For the plugin interfaces and how the engine resolves them, see [Extending a workflow](/docs/pbc/all/back-office/latest/base-shop/workflow-feature-overview.html#extending-a-workflow).

### 6) Register the data importer

Register the Workflow data import plugin so workflows can be provisioned from CSV.

**src/Pyz/Zed/DataImport/DataImportDependencyProvider.php**

```php
use Spryker\Zed\Workflow\Communication\Plugin\DataImport\WorkflowDataImportPlugin;

/**
 * @return array&lt;\Spryker\Zed\DataImport\Dependency\Plugin\DataImportPluginInterface&gt;
 */
protected function getDataImporterPlugins(): array
{
    return [
        new WorkflowDataImportPlugin(),
    ];
}
```

### 7) Provide the import data

The import uses two files: a definition XML file that describes the state machine (its states, transitions, and events, in the same `state-machine-01` format used by the OMS), and a CSV that provisions the workflow and points at that XML file. Storing the definition in its own file keeps the CSV readable and lets you edit the state machine like any other XML process.

The `definition` column in the CSV holds the path to the XML file, relative to the project root.

**data/import/common/common/workflow/company_onboarding.xml**

```xml
{% raw %}&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;statemachine
    xmlns=&quot;spryker:state-machine-01&quot;
    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
    xsi:schemaLocation=&quot;spryker:state-machine-01 http://static.spryker.com/state-machine-01.xsd&quot;
&gt;
    &lt;process name=&quot;CompanyOnboarding&quot; main=&quot;true&quot;&gt;
        &lt;!-- states, transitions, and events --&gt;
    &lt;/process&gt;
&lt;/statemachine&gt;{% endraw %}
```

**data/import/common/common/workflow.csv**

```csv
name,subject_type,description,initial_state,version,definition,trigger_events,is_active
CompanyOnboarding,Company,B2B company onboarding demo workflow,created,1,data/import/common/common/workflow/company_onboarding.xml,&quot;Entity.spy_company.create,Entity.spy_company.update&quot;,1
```

| COLUMN | REQUIRED | DESCRIPTION |
|--------|----------|-------------|
| name | yes | Workflow (process) name. |
| subject_type | yes | The subject the workflow applies to, for example `Company`. |
| description | no | Human-readable description. |
| initial_state | yes | The state a new instance starts in. |
| version | yes | Version number. The importer upserts on `(name, version)`, so re-imports do not create duplicates. |
| definition | yes | Path to the definition XML file, relative to the project root — for example `data/import/common/common/workflow/company_onboarding.xml`. |
| trigger_events | no | The Publish &amp; Synchronize event names custom type of triggers that start an instance, for example `Entity.spy_company.create`. To list several events, separate them with commas and wrap the whole cell in double quotes so the commas are not read as CSV column separators: `&quot;Entity.spy_company.create,Entity.spy_company.update&quot;`. |
| is_active | no | `1` activates this version and its process. |

Run the importer directly to verify:

```bash
docker/sdk cli console data:import workflow
```

### 8) Add the importer to the install recipe

Register the workflow importer in the data import configuration so it runs during deployment. Add it to the region import config that the install recipes invoke through `data:import`.

Add the following entry to the import config of **every region and environment you deploy** — the local files (`data/import/local/full_&lt;REGION&gt;.yml`) and the production files (`data/import/production/full_&lt;REGION&gt;.yml`). Adding it to only one file provisions the workflow only for that region and environment.

**data/import/local/full_EU.yml** (and the other `full_&lt;REGION&gt;.yml` files, local and production)

```yaml
    - data_entity: workflow
      source: data/import/common/common/workflow.csv
```

Place the entry after the modules whose subjects the workflow attaches to (for example `company`), so those subjects exist before the workflow is provisioned. The install recipes already call `data:import` with the region import config, so no recipe change is required beyond this entry.

{% info_block warningBox &quot;Verification&quot; %}

Run the install recipe and make sure the workflow with its versions appears under **Administration &gt; Workflows**.

{% endinfo_block %}

### 9) Schedule the condition and timeout jobs

Condition and timeout transitions have no event, so they must be advanced by two console commands: `workflow:check-condition` and `workflow:check-timeout`. These commands are **not** scheduled out of the box — register them as recurring jobs the same way the OMS and state machine checks are scheduled. Add the following jobs to `config/Zed/cronjobs/jenkins.php`:

```php
$jobs[] = [
    &apos;name&apos; =&gt; &apos;workflow-check-conditions&apos;,
    &apos;command&apos; =&gt; &apos;$PHP_BIN vendor/bin/console workflow:check-condition&apos;,
    &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
    &apos;enable&apos; =&gt; true,
    &apos;stores&apos; =&gt; $allStores,
];

$jobs[] = [
    &apos;name&apos; =&gt; &apos;workflow-check-timeouts&apos;,
    &apos;command&apos; =&gt; &apos;$PHP_BIN vendor/bin/console workflow:check-timeout&apos;,
    &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
    &apos;enable&apos; =&gt; true,
    &apos;stores&apos; =&gt; $allStores,
];
```

{% info_block warningBox &quot;Verification&quot; %}

Start an instance, then run `docker/sdk cli console workflow:check-condition` and confirm the instance advances in `spy_state_machine_process_definition_instance`.

{% endinfo_block %}
</description>
            <pubDate>Tue, 11 Aug 2026 13:42:49 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html</guid>
            
            
        </item>
        
        <item>
            <title>Typed collections in the published contract</title>
            <description>This document explains what an object collection publishes in the OpenAPI contract, and the constraints
that make typing an existing list field a deliberate decision.

For the schema syntax itself, see
[Object collections](/docs/integrations/spryker-api/api-platform/resource-schemas.html#object-collections).
This page covers what that syntax produces for API consumers and what to check before adopting it.

## What an object collection publishes

A `type: array` property whose `items` are a typed object publishes the element as a referenced
component schema, so the contract describes a single element as precisely as it describes a single
nested object.

Given this property on the `Products` resource:

```yaml
prices:
    type: array
    writable: false
    description: &apos;Prices per store and currency&apos;
    items:
        type: object
        properties:
            grossAmount: { type: integer }
            currency:    { type: string }
```

the published contract references a generated element schema:

```json
&quot;prices&quot;: {
    &quot;type&quot;: &quot;array&quot;,
    &quot;description&quot;: &quot;Prices per store and currency&quot;,
    &quot;items&quot;: {
        &quot;$ref&quot;: &quot;#/components/schemas/ProductsPricesBackendObject&quot;
    }
}
```

and registers that schema in the same document, so the reference always resolves:

```json
&quot;ProductsPricesBackendObject&quot;: {
    &quot;type&quot;: &quot;object&quot;,
    &quot;properties&quot;: {
        &quot;grossAmount&quot;: { &quot;type&quot;: &quot;integer&quot; },
        &quot;currency&quot;: { &quot;type&quot;: &quot;string&quot; }
    }
}
```

Without an `items` block, the same property publishes as a bare `&quot;type&quot;: &quot;array&quot;` with no element
description. Consumers cannot see the element shape, and SDK generators produce an untyped collection.

{% info_block infoBox &quot;Reference prefix&quot; %}

The prefix depends on the document flavor. The OpenAPI document uses `#/components/schemas/`, while a
plain JSON Schema document uses `#/definitions/`. Both point at the same generated element schema.

{% endinfo_block %}

### How the element type reaches the contract

PHP has no generics, so the property itself stays a plain `array` and the element type travels in a
docblock on the generated resource class:

```php
/**
 * @var array&lt;\Generated\Api\Backend\Products\ProductsPricesBackendObject&gt;
 */
public array $prices = [];
```

API Platform reads that docblock through its property-metadata chain while building the schema, which is
what lets it emit the reference and register the element schema itself.

This matters when you are debugging: if the docblock is missing from the generated class, the contract
falls back to an untyped array. See
[Troubleshooting](/docs/integrations/spryker-api/api-platform/troubleshooting.html).

## Lists of scalars

A list of scalars needs no element class and produces no reference:

```yaml
skus:
    type: array
    items:
        type: string
```

The property stays a plain typed array. This is complete as it is — there is nothing further to declare.

## Edge cases and constraints

### A typed element is a closed shape

This is the most important constraint on this page.

Generated value objects copy only the fields you declare. Both `fromArray()` and `toArray()` iterate the
declared properties and nothing else. Adding an `items` block to a list that is already part of a
released response therefore **silently drops every payload key you did not declare** in
`items.properties`.

Nothing warns you. The response loses fields, and consumers relying on them break.

For a **new** field this never arises — declare `items` from the start and the shape is complete by
construction.

For an **existing** field, treat adoption as a backward-compatibility decision:

1. Capture a real response payload for the endpoint.
2. List the keys present on a single element of the list.
3. Compare them against the `items.properties` you intend to declare.
4. Add every missing key before adopting, or do not adopt yet.

{% info_block warningBox &quot;Check the payload, not the transfer object&quot; %}

Compare against a real response, not the transfer object behind it. A provider can add, rename, or omit
keys on the way out, so the transfer object is not a reliable description of what consumers receive.

{% endinfo_block %}

### `openapiContext.items` does not type anything

This is the most common mistake, because the two forms look almost identical in YAML.

An `items` block nested under `openapiContext` is documentation passthrough. It describes the element in
the OpenAPI document, but it generates no class and produces no reference:

```yaml
# Documentation only — no generated class, no typed reference
categories:
    type: array
    openapiContext:
        items:
            type: object
            properties:
                categoryKey: { type: string }
```

Only a sibling of `type: array` produces a typed element schema:

```yaml
# Typed — generates a class and publishes a reference
categories:
    type: array
    items:
        type: object
        properties:
            categoryKey: { type: string }
```

### Declaring both forms is rejected

A property must not declare both `items` and `openapiContext.items`. Schema validation rejects the
combination, because `openapiContext` is merged on top of the derived schema: the hand-written shape
would win and the typed element schema would be discarded without a trace.

Keep the `items` sibling and remove the `openapiContext.items` block.

### Relationship properties cannot declare `items`

A property that is a relationship target declared in `includes` must not also declare a sibling `items`
block. Schema validation rejects the combination.

Only one docblock is emitted per property, and a relationship property already receives one describing
its target resource. The two declarations compete for the same slot, so the ambiguity is rejected rather
than silently resolved. A property is either a relationship to another resource or an inline object
list — not both. See [Relationships](/docs/integrations/spryker-api/api-platform/relationships.html).

### Nested lists inside `openapiContext` stay hand-written

A list nested inside an `openapiContext` block is as hand-described as the block containing it. It never
becomes a typed reference, however it is declared, because nothing under `openapiContext` reaches the
generator.

### Providers can keep assigning raw arrays

Declaring `items` does not break a provider that assigns plain arrays instead of value objects. The
generated mapping code guards both directions: `toArray()` maps an element through its `toArray()` only
when the element is an instance of the generated class, and `fromArray()` hydrates an element only when
it is an array. Anything else passes through unchanged.

You can therefore improve a field&apos;s published contract without rewriting its provider at the same time.

### Nullable collections preserve null

A nullable typed collection preserves `null` rather than coercing it to an empty array. `toArray()`
returns `null` when the property is `null`, and `fromArray()` falls back to `null` instead of `[]` when
the key is absent or is not an array.

### Canonical objects behave slightly differently

When the element shape is shared across resources through a canonical object, the collection form and
the single form differ:

- A canonical **single** object is typed directly to the shared class.
- A canonical **collection** stays a plain `array` and names its element class through the docblock, for
  the same reason any collection does — PHP has no generics.

Both publish a reference to the same shared component schema. See
[Project-defined canonical nested objects](/docs/integrations/spryker-api/api-platform/resource-schemas.html#project-defined-canonical-nested-objects).

## Adoption status

No property in the code base declares a sibling `items` block on a resource yet. The capability is
available, and adoption happens per field so each one can be checked against a real payload first, as
described in [A typed element is a closed shape](#a-typed-element-is-a-closed-shape).

If you are looking for an existing example to copy, there is not one yet — use this page and
[Object collections](/docs/integrations/spryker-api/api-platform/resource-schemas.html#object-collections)
instead.
</description>
            <pubDate>Tue, 11 Aug 2026 12:00:31 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/typed-collections.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/typed-collections.html</guid>
            
            
        </item>
        
        <item>
            <title>Troubleshooting API Platform</title>
            <description>This document provides solutions to common issues when working with API Platform in Spryker.

## Generation issues

### Resources not generating

**Symptom:** Running `docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate` completes but no resources are created.

**Possible causes:**

1. **Schema file location is incorrect**

   ```bash
   ❌ src/Pyz/Glue/Customer/api/customers.resource.yml
   ✅ src/Pyz/Glue/Customer/resources/api/backend/customers.resource.yml
   ```

2. **API type not configured**

   Check `config/{APPLICATION}/packages/spryker_api_platform.php`:

   ```php
   return static function (SprykerApiPlatformConfig $sprykerApiPlatform): void {
       $sprykerApiPlatform-&gt;apiTypes([
           &apos;backend&apos;, // Must match directory name
       ]);
   };
   ```

3. **Bundle not registered**

   Verify `config/{APPLICATION}/bundles.php` includes:

   ```php
   SprykerApiPlatformBundle::class =&gt; [&apos;all&apos; =&gt; true],
   ```

**Solution:**

```bash
# Debug to see what&apos;s being discovered
docker/sdk cli glue  api:debug --list

# Check schema validation
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only

# Force regeneration
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --force
```

### Schema validation errors

**Symptom:** Generation fails with schema validation errors.

**Common errors:**

```bash
# Error: Invalid operation type
❌ operations:
    - type: CREATE

✅ operations:
    - type: Post

# Error: Invalid property type
❌ type: int
✅ type: integer

# Error: Missing resource name
❌ resource:
    shortName: customers

✅ resource:
    name: Customers
    shortName: customers

# Error: Property declares both &quot;items&quot; and &quot;openapiContext.items&quot;
# openapiContext is merged on top of the derived schema, so the hand-written
# shape would win and the typed element schema would be discarded silently.
❌ categories:
    type: array
    items:
        type: object
        properties:
            categoryKey: { type: string }
    openapiContext:
        items:
            type: object
            properties:
                categoryKey: { type: string }

✅ categories:
    type: array
    items:
        type: object
        properties:
            categoryKey: { type: string }

# Error: Property is both a relationship and an inline object list
# A relationship property already receives a docblock describing its target
# resource, and only one docblock is emitted per property.
❌ includes:
    - relationshipName: addresses
      targetResource: CustomersAddresses
  properties:
    addresses:
        type: array
        items:
            type: object
            properties:
                city: { type: string }

✅ includes:
    - relationshipName: addresses
      targetResource: CustomersAddresses
```

**Solution:**

1. Check schema against examples in documentation
2. Use `--validate-only` flag for detailed validation:

   ```bash
   docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only
   ```

3. Inspect merged schema:

   ```bash
   docker/sdk cli glue  api:debug resource-name --show-merged
   ```

### A list property still publishes as an untyped array

**Symptom:** A `type: array` property has an `items` block, but the published contract shows
`&quot;type&quot;: &quot;array&quot;` with no `items` reference, and SDK generators produce an untyped collection.

**Possible causes:**

1. **`items` is nested under `openapiContext` instead of being a sibling of `type: array`**

   Only a sibling triggers typing. An `items` block under `openapiContext` is documentation
   passthrough: it generates no class and produces no reference.

   ```yaml
   ❌ categories:
       type: array
       openapiContext:
           items:
               type: object
               properties:
                   categoryKey: { type: string }

   ✅ categories:
       type: array
       items:
           type: object
           properties:
               categoryKey: { type: string }
   ```

2. **`items.type` is a scalar**

   A list of scalars generates no element class and no reference. This is expected — there is nothing
   to type.

3. **The resource was not regenerated after the schema change**

   ```bash
   docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate
   ```

4. **Stale generated code is still being served**

   See [Inspecting generated code](#inspecting-generated-code) to confirm what is on disk.

**Solution:**

Confirm the generated resource class carries a `@var array&lt;\Generated\…&gt;` docblock on the property.
That docblock is what API Platform reads to build the element reference — if it is absent, the contract
falls back to an untyped array. See
[Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html).

## Runtime issues

### Provider/Processor not found

**Symptom:**

```bash
Error: Class &quot;Pyz\Glue\Customer\Api\Backend\Provider\CustomerBackendProvider&quot; not found
```

**Possible causes:**

1. Class doesn&apos;t exist or namespace is wrong
2. Not registered in the Dependency Injection container
3. Typo in the schema file

**Solution:**

1. Verify the class exists and namespace matches:

   ```php
   namespace Pyz\Glue\Customer\Api\Backend\Provider;

   class CustomerBackendProvider implements ProviderInterface
   ```

2. Ensure services are auto-discovered in `ApplicationServices.php`:

   ```php
   $services-&gt;load(&apos;Pyz\\Glue\\&apos;, &apos;../../../src/Pyz/Glue/&apos;);
   ```

3. Check class name in the resource schema file of the module matches exactly:

   ```yaml
   provider: &quot;Pyz\\Glue\\Customer\\Api\\Backend\\Provider\\CustomerBackendProvider&quot;
   ```

### Validation not working

**Symptom:** API accepts invalid data despite validation rules.

**Possible causes:**

1. Validation schema file not found
2. Wrong operation name in validation schema
3. Validation groups not matching

**Solution:**

1. Ensure validation file exists:

   ```bash
   ✅ resources/api/backend/customers.validation.yml
   ```

2. Match operation names to HTTP methods:

   ```yaml
   post:      # For POST /customers
     email:
       - NotBlank

   patch:     # For PATCH /customers/{id}
     email:
       - Optional:
           constraints:
             - Email
   ```

3. Check generated resource class (for example `Generated\Api\Storefront\CustomersStorefrontResource`) has validation attributes:

   ```php
   #[Assert\NotBlank(groups: [&apos;customers:create&apos;])]
   #[Assert\Email(groups: [&apos;customers:create&apos;])]
   public ?string $email = null;
   ```

### API documentation UI not displaying correctly

**Symptom:** When accessing the root URL of your API application, you see:
- Missing styles/CSS
- Broken JavaScript functionality
- Plain HTML without formatting
- &quot;Failed to load resource&quot; errors in the browser console

**Cause:** Assets were not installed after API Platform integration.

**Solution:**

Run the appropriate assets:install command for your application:

#### For Glue application

```bash
docker/sdk cli glue assets:install public/Glue/assets  --symlink
```

#### For GlueStorefront

```bash
docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue assets:install public/GlueStorefront/assets/  --symlink
```

#### For GlueBackend

```bash
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue assets:install public/GlueBackend/assets/  --symlink
```

Then verify the documentation UI loads correctly by visiting the root URL:
- Storefront: `https://glue-storefront.mysprykershop.com/`
- Backend: `https://glue-backend.mysprykershop.com/`

{% info_block warningBox &quot;Required after integration&quot; %}

The `assets:install` command must be run after integrating API Platform and whenever API Platform assets are updated. This is a required step documented in [Integrate API Platform](/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/integrate-api-platform.html).

{% endinfo_block %}

### 404 Not Found for API endpoints

**Symptom:** API requests return 404.

**Possible causes:**

1. Router not configured
2. Routes not loaded
3. Wrong URL format

**Solution:**

1. Verify `SymfonyFrameworkRouterPlugin` is registered:

   ```php
   // RouterDependencyProvider
   protected function getRouterPlugins(): array
   {
       return [
           new GlueRouterPlugin(),
           new SymfonyFrameworkRouterPlugin(), // Must be present
       ];
   }
   ```

2. Check API documentation for correct URLs:

   ```bash
   Storefront: https://glue-storefront.mysprykershop.com/
   Backend: https://glue-backend.mysprykershop.com/
   ```

   The interactive API documentation is available at the root URL of each application.

3. Use correct URL format:

   ```bash
   ❌ /api/v1/customers
   ✅ /customers
   ```

### Requests without an `Accept` header are rejected or return the wrong format

**Symptom:** A client request that omits the `Accept` header — or sends only `Accept: */*` — returns `406 Not Acceptable`, or a response in a format other than the legacy `application/vnd.api+json`. The legacy Glue REST API silently accepted the same request and answered with `application/vnd.api+json`.

**Cause:** API Platform runs content negotiation that requires a satisfiable `Accept` header and does not assume the legacy Glue default. This is a behavioral difference from the legacy Glue REST stack.

**Solution:**

1. Upgrade `spryker/api-platform` to **1.15.0 or higher**. Its `AcceptHeaderFallbackSubscriber` restores the legacy behavior — a missing or `*/*` `Accept` header defaults to `application/vnd.api+json`:

   ```bash
   composer update spryker/api-platform --with-dependencies
   ```

2. If you cannot upgrade, send an explicit `Accept` header from the client:

   ```bash
   curl -H &quot;Accept: application/vnd.api+json&quot; https://glue-backend.mysprykershop.com/customers
   ```

### Pagination not working

**Symptom:** All results returned instead of paginated response.

**Solution:**

1. Enable pagination in the schema file of the defining module:

   ```yaml
   resource:
     paginationEnabled: true
     paginationItemsPerPage: 10
   ```

2. Return `PaginatorInterface` from provider:

   ```php
   use ApiPlatform\State\Pagination\TraversablePaginator;

   return new TraversablePaginator(
       new \ArrayObject($results),
       $currentPage,
       $itemsPerPage,
       $totalItems
   );
   ```

3. Use pagination query parameters:

   ```bash
   GET /customers?page=2&amp;itemsPerPage=20
   ```

### Client cannot change items per page

**Symptom:** The `itemsPerPage` query parameter is ignored.

**Solution:**

Enable client-side items-per-page control and set a maximum limit in the resource schema:

```yaml
resource:
  paginationEnabled: true
  paginationItemsPerPage: 10
  paginationClientItemsPerPage: true
  paginationMaximumItemsPerPage: 100
```

Without `paginationClientItemsPerPage: true`, the `itemsPerPage` query parameter has no effect. The `paginationMaximumItemsPerPage` option prevents clients from requesting excessively large pages.

### Client cannot disable pagination

**Symptom:** The `pagination=false` query parameter is ignored and results are still paginated.

**Solution:**

Enable client-side pagination control in the resource schema:

```yaml
resource:
  paginationClientEnabled: true
```

Without `paginationClientEnabled: true`, the `pagination` query parameter has no effect.

For a full reference of all pagination options, see [Resource schemas — Pagination](/docs/integrations/spryker-api/api-platform/resource-schemas.html#pagination).

## Dependency Injection issues

### Services not autowired

**Symptom:**

```bash
Cannot autowire service &quot;CustomerBackendProvider&quot;: argument &quot;$customerFacade&quot;
references class &quot;CustomerFacadeInterface&quot; but no such service exists.
```

**Solution:**

1. Register facade in the respective applications `ApplicationServices.php`:

   ```php
   use Pyz\Zed\Customer\Business\CustomerFacadeInterface;
   use Pyz\Zed\Customer\Business\CustomerFacade;

   $services-&gt;set(CustomerFacadeInterface::class, CustomerFacade::class);
   ```

2. Ensure constructor uses interface type hints:

   ```php
   public function __construct(
       private CustomerFacadeInterface $customerFacade,  // ✅ Interface
   ) {}
   ```

## Performance issues

### Slow API responses

**Symptom:** API endpoints respond slowly.

**Solution:**

1. Verify that Opcache is enabled (`opcache.enable: 1`). Without it, PHP recompiles the whole application on every request, which adds a flat overhead of seconds to every endpoint regardless of the amount of data. See [Opcache activation](/docs/dg/dev/guidelines/performance-guidelines/general-performance-guidelines.html#opcache-activation).
2. Enable Symfony cache:

   ```bash
   docker/sdk cli glue  cache:warmup
   ```

3. Use pagination for collections
4. Optimize database queries in Provider
5. Use API Platform&apos;s built-in caching features

## Development tips

### Debugging schema merging

See which schemas contribute to final resource:

```bash
docker/sdk cli glue  api:debug customers --api-type=backend --show-sources
```

Output:

```bash
Source Files (priority order):
  ✓ vendor/spryker/customer/resources/api/backend/customers.resource.yml (CORE)
  ✓ src/SprykerFeature/CRM/resources/api/backend/customers.resource.yml (FEATURE)
  ✓ src/Pyz/Glue/Customer/resources/api/backend/customers.resource.yml (PROJECT)
```

### Inspecting generated code

View the generated resource class:

```bash
cat src/Generated/Api/Backend/CustomersBackendResource.php
```

Check for:
- Correct property types
- Validation attributes
- API Platform metadata

### Testing with dry-run

Preview generation without writing files:

```bash
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --dry-run
```

## Getting help

If you encounter issues not covered here:

1. **Check logs:**

   ```bash
   tail -f var/log/application.log
   tail -f var/log/exception.log
   ```

2. **Enable debug mode:**

   ```php
   &lt;?php

   // config/{APPLICATION}/packages/spryker_api_platform.php

   declare(strict_types = 1);

   use Symfony\Config\SprykerApiPlatformConfig;

   return static function (SprykerApiPlatformConfig $sprykerApiPlatform): void {
       $sprykerApiPlatform-&gt;debug(true);
   };
   ```

3. **Validate environment:**

   ```bash
   php -v  # Check PHP version (8.3+)
   composer show | grep api-platform
   docker/sdk cli glue  debug:container | grep -i api
   ```

4. **Common error patterns:**

| Error | Likely cause | Solution |
|-------|--------------|----------|
| `Class not found` | Autoloading issue | Run `composer dump-autoload` |
| `Service not found` | DI configuration | Check `ApplicationServices.php` |
| `Route not found` | Router not configured | Add `SymfonyFrameworkRouterPlugin` |
| `Validation failed` | Schema mismatch | Regenerate with `--force` |
| `Cache is stale` | Outdated cache | Run `cache:clear` |
| API docs UI broken/unstyled | Assets not installed | Run `docker/sdk cli glue assets:install` |

## Next steps

- [API Platform](/docs/integrations/spryker-api/api-platform/api-platform.html) - Overview and concepts
- [Integrate API Platform](/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/integrate-api-platform.html) - Setup guide
- [Implement an API Platform resource](/docs/integrations/spryker-api/api-platform/enablement.html) - Creating resources
- [Resource schemas](/docs/integrations/spryker-api/api-platform/resource-schemas.html) - Resource schema reference
- [Validation schemas](/docs/integrations/spryker-api/api-platform/validation-schemas.html) - Validation schema reference
- [Test API Platform resources](/docs/integrations/spryker-api/api-platform/testing.html) - Testing guide
</description>
            <pubDate>Tue, 11 Aug 2026 12:00:31 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/troubleshooting.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/troubleshooting.html</guid>
            
            
        </item>
        
        <item>
            <title>Resource schemas</title>
            <description>This document explains how to define API Platform resource schemas in Spryker.

## Schema file structure

API Platform uses YAML files to define resource schemas. Resource schemas describe the structure, operations, and behavior of your API resources.

### Schema location

Resource schemas must be placed in the `resources/api/{api-type}/` directory within your module:

```MARKDOWN
src/
├── Spryker/
│   └── {Module}/
│       └── resources/
│           └── api/
│               ├── storefront/
│               │   └── resource-name.resource.yml
│               └── backend/
│                   └── resource-name.resource.yml
├── SprykerFeature/
│   └── {Feature}/
│       └── resources/
│           └── api/
│               └── backend/
│                   └── resource-name.resource.yml
└── Pyz/
    └── Glue/
        └── {Module}/
            └── resources/
                └── api/
                    └── backend/
                        └── resource-name.resource.yml
```

## CodeBucket resources

API Platform supports CodeBucket-specific resource variants that are resolved at runtime based on the `APPLICATION_CODE_BUCKET` environment constant. A variant keeps the same file name as the base schema but lives in a separate variant module directory—for example, `StoresApiEU`—and sets the `codeBucket:` property inside the schema file. The generator produces one class per variant following the `{ResourceName}{CodeBucket}{ApiType}Resource` pattern, and the base resource is used when no matching variant exists. For file naming, class naming, URL behavior, and implementation examples, see [CodeBucket support](/docs/integrations/spryker-api/api-platform/code-buckets.html).

## Resource schema syntax

### Minimal example

```yaml
resource:
  name: Products
  shortName: products
  description: &quot;Product resource&quot;

  operations:
    - type: Get
    - type: GetCollection

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

    name:
      type: string
```

{% info_block infoBox &quot;shortName convention&quot; %}

`shortName` is the JSON:API `type` field for the resource and is used as the public URL segment. Use **lowercase kebab-case**, plural for noun-style resources (`products`, `addresses`, `abstract-product-prices`) and singular for action-style endpoints (`catalog-search`, `cart-reorder`). Multi-word names are always hyphenated. This matches every shipped resource in the platform.

{% endinfo_block %}

### Complete example with all options

```yaml
# yaml-language-server: $schema=../../../../../vendor/spryker/api-platform/resources/schemas/api-resource-schema-v1.json

resource:
  # Resource identification
  name: Customers                    # Internal name (used for schema merging)
  shortName: customers               # URL name (becomes /customers); JSON:API type field
  description: &quot;Customer resource&quot;   # OpenAPI description

  # State providers and processors
  provider: &quot;Pyz\\Glue\\Customer\\Api\\Backend\\Provider\\CustomerBackendProvider&quot;
  processor: &quot;Pyz\\Glue\\Customer\\Api\\Backend\\Processor\\CustomerBackendProcessor&quot;

  # Pagination configuration
  paginationEnabled: true
  paginationItemsPerPage: 10
  paginationMaximumItemsPerPage: 100
  paginationClientEnabled: true
  paginationClientItemsPerPage: true

  # JSON:API `included` array ordering — see &quot;Sort priority for included resources&quot;
  includedSortPriority: 0

  # Security
  security: &quot;is_granted(&apos;ROLE_ADMIN&apos;)&quot;
  securityPostDenormalize: &quot;is_granted(&apos;EDIT&apos;, object)&quot;

  # Operations
  operations:
    - type: Post                     # Create new resource
    - type: Get                      # Get single resource
    - type: GetCollection            # Get collection with pagination
    - type: Put                      # Replace entire resource
    - type: Patch                    # Update partial resource
    - type: Delete                   # Delete resource

  # Relationships — see Relationships article for full reference
  includes:
    - relationshipName: addresses
      targetResource: CustomersAddresses
      uriVariableMappings:
        customerReference: customerReference

  # Properties
  properties:
    idCustomer:
      type: integer
      description: &quot;The unique identifier of the customer.&quot;
      writable: false                # Read-only property
      readable: true                 # Include in responses (default: true)

    email:
      type: string
      description: &quot;The email address.&quot;
      required: true                 # Required for all operations
      openapiContext:
        example: &quot;john@example.com&quot;
        format: &quot;email&quot;

    firstName:
      type: string
      description: &quot;First name.&quot;
      openapiContext:
        example: &quot;John&quot;
        minLength: 1
        maxLength: 100

    status:
      type: string
      description: &quot;Customer status.&quot;
      openapiContext:
        example: &quot;active&quot;
        schema:
          enum: [&quot;active&quot;, &quot;inactive&quot;, &quot;pending&quot;]

    customerReference:
      type: string
      description: &quot;Unique customer reference.&quot;
      writable: false
      identifier: true               # Use as URL identifier instead of @id

    dateOfBirth:
      type: string
      description: &quot;Date of birth.&quot;
      openapiContext:
        format: &quot;date&quot;
        example: &quot;1990-01-01&quot;

    isActive:
      type: boolean
      description: &quot;Active status.&quot;
      default: true

    creditLimit:
      type: number
      description: &quot;Credit limit.&quot;
      openapiContext:
        format: &quot;float&quot;
        example: 5000.00
```

## Property types

### Supported types

| Type | PHP Type | Example | Description |
|------|----------|---------|-------------|
| `string` | `string` | `&quot;John&quot;` | Text values |
| `integer` | `int` | `42` | Whole numbers |
| `number` | `float` | `3.14` | Decimal numbers |
| `boolean` | `bool` | `true` | True/false values |
| `array` | `array` | `[&quot;a&quot;, &quot;b&quot;]` | Lists of values. Add an `items` sibling to publish a typed element schema instead of an untyped array — see [Object collections](#object-collections) and [Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html). |
| `object` | `object` | `{&quot;key&quot;: &quot;value&quot;}` | Strictly typed nested objects — generates a typed companion class. See [Typed nested objects](#typed-nested-objects). A project can also share one shape across resources with a [canonical nested object](#project-defined-canonical-nested-objects). |
| `map` | `array` | `{&quot;key&quot;: &quot;value&quot;}` | Free-shape associative payloads documented via `openapiContext`. Stored as PHP `array` and rendered as `type: object` in the OpenAPI specification. |
| `mixed` | `mixed` | any | Use only when the payload genuinely has no fixed shape and cannot be described via `openapiContext`. |

Use `map` when the payload is a structured JSON object whose schema you want to describe via
`openapiContext` rather than a strongly typed PHP class. This is the recommended type whenever a
request or response body is a JSON object with a known shape but no dedicated class — it
keeps the property typed as a simple `array` in PHP while still producing rich OpenAPI metadata
and a working &quot;Try Out&quot; body in Swagger UI. See
[Documenting nested properties for OpenAPI and Swagger UI](#documenting-nested-properties-for-openapi-and-swagger-ui)
for the full pattern.

When you do want a strongly typed class for the payload — so PHP enforces the field set and the
OpenAPI document publishes a named component schema — use `type: object` with nested
`properties:` instead. See [Typed nested objects](#typed-nested-objects).

### Property attributes

#### writable

Controls if property can be sent in requests (POST/PUT/PATCH):

```yaml
password:
  type: string
  writable: true    # Can be sent in requests
  readable: false   # Not included in responses
```

#### readable

Controls if property is included in responses:

```yaml
idCustomer:
  type: integer
  writable: false   # Cannot be modified
  readable: true    # Included in responses
```

#### identifier

Marks property as URL identifier:

```yaml
customerReference:
  type: string
  identifier: true  # URL becomes /customers/{customerReference}
```

#### required

Makes property mandatory (use validation schemas for detailed rules):

```yaml
email:
  type: string
  required: true    # Must be present
```

#### default

Sets default value:

```yaml
isActive:
  type: boolean
  default: true     # Defaults to true if not provided
```

## Typed nested objects

A property declared as `type: object` with its own nested `properties:` block generates a
dedicated, strongly typed companion class — not an untyped array. The generator emits one PHP
class per nested object, types the parent property to that class, and publishes a full
field-by-field schema in the OpenAPI document. The serializer hydrates the nested object from the
same JSON payload, so the response on the wire is identical to the array-based form it replaces.

This is the strongly typed counterpart to the `map` pattern described in
[Documenting nested properties for OpenAPI and Swagger UI](#documenting-nested-properties-for-openapi-and-swagger-ui):
`map` documents a nested object while keeping it a plain PHP `array`; `type: object` promotes it
to a real class whose shape is enforced by PHP&apos;s type system.

### Why use it

- **Type safety in PHP.** The parent property is typed to the generated class (for example,
  `?CartsTotalsStorefrontObject`) instead of `array`, so providers and processors get IDE
  autocompletion and the language enforces the field set.
- **Precise OpenAPI schema.** Each sub-field carries its own `type`, `description`, and `example`,
  so the OpenAPI document and Swagger UI render the object as a named component schema instead of
  an opaque `object`.
- **No runtime contract change.** Because the serializer denormalizes the typed object from the
  same keys, migrating a property from `array`/`map` to `type: object` leaves the JSON response
  unchanged — only the generated PHP and the published schema improve.

### When to use which type

| Use | When |
|-----|------|
| `type: object` (with `properties`) | The payload has a **stable, known shape** you want enforced as a PHP class — for example, cart and order `totals`, or a quote-request `customer`. |
| `type: map` (with `openapiContext`) | The shape is known and worth documenting, but you do **not** want a dedicated PHP class — for example, payloads aggregated from several transfer objects, or PSP-specific responses. See [Documenting nested properties for OpenAPI and Swagger UI](#documenting-nested-properties-for-openapi-and-swagger-ui). |
| `type: mixed` | The payload genuinely has **no fixed shape** and cannot be described via `openapiContext`. |

### How to declare it

Give the property `type: object` and nest its fields under `properties:`. Sub-fields accept the
same attributes as top-level properties (`type`, `description`, `openapiContext`, `nullable`,
`serializedName`, `serializedPath`):

```yaml
totals:
    type: object
    readable: true
    writable: false
    required: false
    description: &apos;Calculated cart totals in cents.&apos;
    properties:
        subtotal:
            type: integer
            description: &apos;Items × prices before any discount/tax.&apos;
            openapiContext: { example: 16058 }
        grandTotal:
            type: integer
            description: &apos;What the customer pays.&apos;
            openapiContext: { example: 14601 }
        priceToPay:
            type: integer
            description: &apos;Grand total adjusted for any pre-paid amount (e.g. gift cards).&apos;
            openapiContext: { example: 14601 }
```

### Generated output

For a `Carts` resource with the `totals` property above, the generator:

1. Types the property on the resource class:

   ```php
   public ?CartsTotalsStorefrontObject $totals = null;
   ```

2. Writes a companion class in the `Generated\Api\{ApiType}\{ResourceName}\` namespace (a
   sub-namespace named after the owning resource, alongside the resource class in
   `Generated\Api\{ApiType}\`). The class is `final`, carries **no** `#[ApiResource]` attribute —
   it is an embedded value object, not a routed resource — and exposes the typed sub-fields plus
   their accessors:

   ```php
   namespace Generated\Api\Storefront\Carts;

   use ApiPlatform\Metadata\ApiProperty;

   final class CartsTotalsStorefrontObject
   {
       #[ApiProperty(description: &apos;Items × prices before any discount/tax.&apos;, openapiContext: [&apos;example&apos; =&gt; 16058])]
       public ?int $subtotal = null;

       #[ApiProperty(description: &apos;What the customer pays.&apos;, openapiContext: [&apos;example&apos; =&gt; 14601])]
       public ?int $grandTotal = null;

       #[ApiProperty(description: &apos;Grand total adjusted for any pre-paid amount (e.g. gift cards).&apos;, openapiContext: [&apos;example&apos; =&gt; 14601])]
       public ?int $priceToPay = null;

       // Getters, setters, toArray(), fromArray() …
   }
   ```

The companion class name is `{ResourceName}{PropertyPath}{ApiType}Object` — the resource&apos;s
normalized name, the capitalized property path, the API type, and the `Object` suffix (contrast
the routed resource class itself, which keeps the `Resource` suffix). It lives in the
`Generated\Api\{ApiType}\{ResourceName}` sub-namespace. So `Carts` + `totals` on the storefront API
becomes `Generated\Api\Storefront\Carts\CartsTotalsStorefrontObject`; a checkout `billingAddress`
becomes `Generated\Api\Storefront\Checkout\CheckoutBillingAddressStorefrontObject`.

{% info_block infoBox &quot;Imports in companion classes&quot; %}

Companion classes import only the attributes they actually use (`ApiProperty`, `SerializedName`,
`SerializedPath`). An attribute referenced without its `use` statement would resolve to a
non-existent class in the `Generated` namespace and break attribute reflection at runtime, so the
generator never emits an unused import.

{% endinfo_block %}

### Nested objects within objects

Objects can nest to any depth. Each level generates its own class, named by concatenating the
property path onto the resource name. For example:

```yaml
totals:
    type: object
    properties:
        tax:
            type: object
            properties:
                amount:
                    type: integer
                    description: &apos;Tax amount in cents.&apos;
                    openapiContext: { example: 1457 }
```

on the storefront `Carts` resource generates a `CartsTotalsStorefrontObject` class with
`public ?CartsTotalsTaxStorefrontObject $tax = null;`, plus a separate
`CartsTotalsTaxStorefrontObject` class with `public ?int $amount = null;` (both in the
`Generated\Api\Storefront\Carts` namespace). A deeper path simply keeps concatenating — an agent
quote-request resource&apos;s `shownVersion.cartTotals` object becomes
`AgentQuoteRequestsShownVersionCartTotalsStorefrontObject`.

### Object collections

A `type: array` property whose `items:` are themselves a typed object (`type: object` with nested
`properties:`) generates a value-object class for the element type. The class is named after the
**pluralized** field segment — `{ResourceName}{PluralField}{ApiType}Object` — and the parent
property stays a PHP `array` carrying a `@var array&lt;…&gt;` docblock so the serializer denormalizes
each element into the generated class:

```yaml
# carts.resource.yml — a list of typed customer objects
customer:
    type: array
    items:
        type: object
        properties:
            firstName: { type: string }
            email:     { type: string }
```

On the storefront `Carts` resource this generates `CartsCustomersStorefrontObject` (the field
`customer` pluralized to `Customers`) as the element type, and types the property as
`array&lt;\Generated\Api\Storefront\Carts\CartsCustomersStorefrontObject&gt;`.

In the published contract, the property becomes `&quot;type&quot;: &quot;array&quot;` with an `items` reference to the
generated element schema, which API Platform registers in the same document. Without an `items` block,
the property publishes as a bare array with no element description.

{% info_block warningBox &quot;Typing an existing list is a backward-compatibility decision&quot; %}

Generated value objects copy only the fields you declare, so adding an `items` block to a list that is
already part of a released response silently drops every payload key missing from `items.properties`.
Check a real payload first — see
[Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html).

{% endinfo_block %}

### Per-resource validation lifting

Each typed nested object gets its **own** value-object class, so validation you authored the
array-shaped way — an `Assert\Collection` on the object property in the resource&apos;s
`{resource-name}.validation.yml` — would reject the denormalized object value with a 422
(`This value should be of type array`). The generator resolves this automatically: for a writable
object property it **lifts** the `Collection.fields` constraints off the property and onto the
matching fields of that resource&apos;s value object, and emits a plain `#[Assert\Valid]` cascade
(carrying the operation groups) on the property instead of the `Collection`.

You keep authoring validation exactly as before — write the `Collection` against the object
property:

```yaml
# checkout-data.validation.yml
post:
    customer:
        - Optional:
              constraints:
                  - Collection:
                        allowExtraFields: true
                        fields:
                            email:
                                - NotBlank: { message: &apos;Email is invalid.&apos; }
                                - Email:    { message: &apos;Email is invalid.&apos; }
```

The lifted constraints are re-grouped through the resource&apos;s own operation groups (so this
`checkout-data` `customer.email` rule stays in the `checkout-data:create` group) and attached to
the value object&apos;s `email` field; the `customer` property itself carries only `#[Assert\Valid]`.
Each resource&apos;s value object is validated independently — there is **no** cross-resource union,
because every resource has its own value-object class. A property whose object is not writable, or
a plain list property that is not a typed object collection, keeps its array-shaped `Collection` —
only writable typed-object properties are lifted.

#### `allowMissingFields`

A `Collection` with `allowMissingFields: true` (for example, a checkout `billingAddress` referenced
only by id) tolerates absent keys. On a value object an absent field denormalizes to `null`, so the
generator relaxes presence constraints when lifting: each `NotBlank` gains `allowNull: true` and
each `NotNull` is dropped — an absent field passes, a present-but-empty one still fails.

### Cross-module field contribution

Because each resource owns its value-object class, a nested object&apos;s fields can still be
contributed from several modules — this is how you keep the dependency direction correct, with
each field declared in its owning module. Multiple modules ship a same-named `*.resource.yml`
fragment for the same resource, and the schema merger **deep-merges nested object `properties`**
(and `items.properties` for collections) rather than letting a later fragment&apos;s nested block
replace an earlier one.

For example, both `DiscountsRestApi` and `ProductOptionsRestApi` add fields to the cart-items
`calculations` object:

```yaml
# DiscountsRestApi — cart-items.resource.yml
resource:
    name: CartItems
    properties:
        calculations:
            type: object
            properties:
                discountTotal: { type: integer }

# ProductOptionsRestApi — cart-items.resource.yml
resource:
    name: CartItems
    properties:
        calculations:
            type: object
            properties:
                productOptionTotal: { type: integer }
```

The merged `calculations` object carries **both** `discountTotal` and `productOptionTotal`, and a
single `CartItemsCalculationsStorefrontObject` value object is generated for it. This deep merge —
not a shared class — is how identically-named objects accumulate fields across modules while each
resource keeps its own independent request/response shape.

#### Conflicting shapes fail generation

Deep merge only applies when the contributors agree on the shape. When one contributor declares a
property as a typed object (`type: object` with `properties`) or an object collection (`type: array`
with `items.properties`) and another declares the **same** property as something structurally
different — a `map`, a scalar, a plain array, or an object without `properties` — a silent
last-wins merge would drop either the typed value object or the plain field. Instead, generation
**fails with an error** that names the property and both contributing source files:

```text
Conflicting shapes for property &quot;calculations&quot;: .../DiscountsRestApi/.../cart-items.resource.yml
declares it as a typed object (`type: object` with `properties`), but
.../project/.../cart-items.resource.yml declares it as `type: map`. ...
```

This applies both within a layer and across layers (project overrides feature overrides core). The
usual cause is a project fragment that still declares a property as `type: map`/`array` while a core
module has since promoted it to a typed object — convert the project fragment to the typed form.
Same-shape overrides (object + object, collection + collection) still deep-merge, and attribute-only
overrides (an override that sets, for example, `writable: false` without re-declaring `type`) merge
as before.

If you deliberately intend to re-shape an inherited property — for example, collapse a core typed
object back into a `map`, or replace it wholesale rather than extend it — set `replace: true` on the
overriding declaration. It takes your declaration wholesale (the inherited one is discarded),
suppresses the conflict guard, and is stripped from the generated output:

```yaml
# project cart-items.resource.yml — deliberately override the core shape
calculations:
    type: map
    replace: true
```

## Project-defined canonical nested objects

[Typed nested objects](#typed-nested-objects) generate one value-object class **per resource
property**: a `billingAddress` on the checkout resource and a `shippingAddress` on the order
resource each get their own independent class, even when both describe the same real-world shape.
That keeps each resource self-contained, but it also means the same address shape is authored and
maintained in several places.

A **canonical nested object** lets a project define that shared shape **once** and have it flow
into every resource property that opts in. All the opting-in properties then collapse onto a single
generated class — `Generated\Api\{ApiType}\{Object}` (for example, `Generated\Api\Storefront\Address`) —
instead of a per-resource companion class.

This is a pure project opt-in. With no canonical object files present, generation is byte-for-byte
identical to the default per-resource behavior described above — nothing changes until a project
adds its first `*.object.yml`.

### File location and naming

Canonical objects live in a **dedicated, reserved subdirectory literally named `objects/`** inside the per-`apiType` resource directory. The directory name is always `objects` — it is never named after a resource or module. This is distinct from resource definition files, which live directly in the `apiType` directory:

```text
resources/api/storefront/
├── checkout.resource.yml          # a resource definition
├── checkout.validation.yml        # its validation
└── objects/                       # reserved dir — canonical objects only
    ├── address.object.yml
    └── address.object.validation.yml
```

Only `*.object.yml` and `*.object.validation.yml` files belong in `objects/`. Resource files (`*.resource.yml`) are placed directly in the per-`apiType` directory, never inside `objects/`.

The `&lt;dashed-name&gt;.&lt;kind&gt;.yml` naming pattern is the same for both file types — only the kind word differs. `address.object.yml` is the canonical-object analog of `checkout.resource.yml`, and `address.object.validation.yml` is the analog of `checkout.validation.yml`. The `object` versus `resource` word identifies the artifact kind, not a different naming scheme.

Full path patterns:

```text
resources/api/&lt;apiType&gt;/objects/&lt;dashed-name&gt;.object.yml
resources/api/&lt;apiType&gt;/objects/&lt;dashed-name&gt;.object.validation.yml   # optional, see Validation
```

For example, on the storefront API:

```text
src/Pyz/resources/api/storefront/objects/address.object.yml
src/Pyz/resources/api/storefront/objects/address.object.validation.yml
```

The file name uses a dashed (kebab-case) object name, while `object.name` **inside** the file is
CamelCase. The CamelCase `object.name` is the contract: it must exactly match the `objectName:`
join tag declared on the resource properties that want this shape (see [The `objectName` join
tag](#the-objectname-join-tag)).

### Central directory

A project may keep canonical object files in one central location instead of (or in addition to) the per-module `objects/` directories. Both locations are scanned simultaneously.

Configure the central directory via the Symfony bundle config node `spryker_api_platform.canonical_object_search_directories`, keyed by API type. Relative paths resolve against the project root; `%kernel.project_dir%` is also supported:

```yaml
# config/packages/spryker_api_platform.yaml
spryker_api_platform:
    canonical_object_search_directories:
        storefront:
            - &apos;%kernel.project_dir%/config/api/objects/storefront&apos;
```

The same `*.object.yml` / `*.object.validation.yml` naming rules apply. Files in a central directory are always treated as the **project** layer, so they participate in the standard `project &gt; feature &gt; core` merge precedence.

Defining the same `objectName` more than once within the same layer — for example, one module file and one central-directory file both at project layer — is a fail-loud error: generation aborts with an `ApiSchemaGenerationException` naming both source files. The same name across different layers is fine — that is the normal override.

### File format

The file contains a single top-level `object:` key:

```yaml
# address.object.yml
object:
    name: Address                                   # CamelCase; matches `objectName: Address` on resource properties
    properties:
        salutation: { type: string, description: &apos;Address salutation.&apos;, example: &apos;Mr&apos; }
        firstName:  { type: string, description: &apos;First name.&apos;, example: &apos;Jane&apos; }
        lastName:   { type: string, description: &apos;Last name.&apos;, example: &apos;Doe&apos; }
        address1:   { type: string, description: &apos;Street name.&apos;, example: &apos;Julie-Wolfthorn-Straße&apos; }
        zipCode:    { type: string, description: &apos;ZIP / postal code.&apos;, example: &apos;10115&apos; }
        city:       { type: string, description: &apos;City.&apos;, example: &apos;Berlin&apos; }
```

| Key | Type | Required | Description |
|-----|------|----------|-------------|
| `object.name` | string | Yes | CamelCase object name. Matched against `objectName:` join tag on every resource property that references this object. |
| `object.properties` | map | Yes | Field definitions. Each field uses the **same syntax as a resource property** — `type`, `description`, `validation`, `example`, and so on. |
| `object.extends` | string | No | CamelCase name of another canonical object whose resolved fields are inherited first. See [Composition](#composition-with-extends-and-omit). |
| `object.omit` | string[] | No | Names of inherited fields to drop from the `extends` base before this object&apos;s own properties are applied. |

### Composition with `extends` and `omit`

An object can inherit another canonical object&apos;s fields with `extends`, then trim and extend them.
This avoids re-declaring a shared shape when one variant is a near-copy of another — for example, a
read-only address snapshot derived from a writable address:

```yaml
# address-snapshot.object.yml
object:
    name: AddressSnapshot
    extends: Address                                # inherit all Address fields first
    omit: [id, idCompanyBusinessUnitAddress]        # drop the write-only identifiers
    properties:
        country: { type: string, description: &apos;Country name.&apos;, example: &apos;Germany&apos; }   # add a read-only field
```

Fields resolve in this order, with later steps winning:

1. The fields inherited from `extends`.
2. Any field named in `omit` is removed.
3. This object&apos;s own `properties` are applied — a field redeclared here overrides the inherited one.

An `extends` cycle (for example, two objects that extend each other) is rejected at generation time
with an `ApiSchemaGenerationException`.

### The `objectName` join tag

A resource property opts into a canonical object by declaring `type: object` together with an
`objectName:` tag whose value equals the canonical `object.name`:

```yaml
# checkout.resource.yml
properties:
    billingAddress:
        type: object
        objectName: Address       # joins this property to the canonical Address object
        readable: false
        writable: true
        properties:
            zipCode: { type: string }
```

The `objectName` tag is dormant on its own: if no `address.object.yml` exists, the property&apos;s
inline `properties:` block is generated exactly as a normal [typed nested
object](#typed-nested-objects). When a canonical file for `Address` **is** present, the tag
activates and:

- The property&apos;s inline `properties:` are **replaced** by the canonical object&apos;s resolved shape.
- The mount attributes — `readable`, `writable`, `required`, `nullable` — stay on the referencing
  property. They describe how this property is mounted on this resource and are **not** owned by
  the canonical object, so the same canonical shape can be writable on one resource and read-only
  on another.
- A single shared `Generated\Api\{ApiType}\{Object}` class is emitted for the canonical object. No
  per-property companion class is generated for that property; every property tagged with the same
  `objectName` is typed to the one shared class.

{% info_block infoBox &quot;Shared class versus per-resource class&quot; %}

Without `objectName`, each `type: object` property generates its own per-resource value-object
class (for example, `CheckoutBillingAddressStorefrontObject`). With `objectName: Address`, all
matching properties across all resources instead share the single `Generated\Api\Storefront\Address`
class. Use a canonical object when several resources genuinely share one shape and you want them to
stay in lockstep; keep the inline form when each resource&apos;s shape is independent.

{% endinfo_block %}

### Validation

Field-level validation for a canonical object is authored in a parallel
`&lt;dashed-name&gt;.object.validation.yml` file, using the same format as a resource
[validation schema](/docs/integrations/spryker-api/api-platform/validation-schemas.html):

```yaml
# address.object.validation.yml
zipCode:
    - NotBlank: { message: &apos;ZIP code is required.&apos; }
firstName:
    - NotBlank: { message: &apos;First name is required.&apos; }
```

These constraints are lifted onto the generated canonical class. Every resource property that
references the object through `objectName` then carries an `Assert\Valid` cascade to that class, so
the canonical field rules are enforced wherever the object is used — you author the object&apos;s
validation once, in one place.

### Layer precedence

Canonical objects follow the same layer rules as resource schemas. The layer is detected from the
file path — a `/Pyz/` path is a project file, a `/SprykerFeature/` path is a feature file, and
anything else is core. Same-named objects merge by `object.name` with the precedence:

```text
project &gt; feature &gt; core
```

Because the merge is by `objectName`, a project can add a single field to a feature-layer canonical
object without redefining the whole object. Core ships no canonical object files today; the
mechanism is available to the project, feature, and core layers, and in practice projects are the
primary users.

## Documenting nested properties for OpenAPI and Swagger UI

Many endpoints accept or return structured JSON payloads — for example, a payment initialization
request that takes `payment`, `quote`, and `customer` sub-objects. Without explicit metadata,
those payloads appear as opaque `object` entries in the OpenAPI document, which means:

- The generated OpenAPI specification does not describe the child fields, their types, or which
  ones are required.
- The Swagger UI &quot;Try Out&quot; button shows an empty request body, forcing consumers to read code or
  external documentation to discover the expected shape.

The `map` property type combined with nested `openapiContext` entries closes both gaps.

### When to use this pattern

Use this pattern when the request or response body is a structured JSON object whose schema you
want to publish through OpenAPI, but you do not want to introduce a dedicated typed PHP class
for it. Typical cases are:

- Request payloads that aggregate fields from multiple transfer objects (for example, payment
  selection plus quote context).
- PSP- or provider-specific response payloads whose shape varies by configuration.

For payloads with a stable, strongly typed shape, prefer `type: object` so the generated PHP
class enforces the structure at the language level.

### Pattern

Combine `type: map` on the property with the following entries inside `openapiContext`:

| Entry | Purpose |
|-------|---------|
| `properties` | Declares each child field with its own `type`, `description`, `format`, and `example`. Used by Swagger UI to render the field-by-field schema. |
| `required` | Lists the child fields that must be present on a request. Drives the &quot;required&quot; markers in Swagger UI and the OpenAPI specification. |
| `example` | A complete sample payload. This is the value Swagger UI prefills into the &quot;Try Out&quot; body, so consumers can execute the request immediately. |

When the property is a `map`, the generator merges `&apos;type&apos; =&gt; &apos;object&apos;` into the emitted
`openapiContext`, so the property appears as an object — with the documented schema — in the
OpenAPI document while staying as a plain PHP `array` in the generated resource class.

### Worked example

The following extract is taken from
`src/Spryker/PaymentsRestApi/resources/api/storefront/payments.resource.yml`. It shows three
common shapes: a flat request object (`payment`), a request object with nested object children
(`quote`), and a response-only object whose contents vary at runtime (`preOrderPaymentData`).

```yaml
properties:
    payment:
        type: map
        writable: true
        readable: false
        required: true
        description: &apos;Payment selection for the pre-order initialization&apos;
        openapiContext:
            required: [&apos;paymentProviderName&apos;, &apos;paymentMethodName&apos;, &apos;amount&apos;]
            properties:
                paymentProviderName:
                    type: string
                    example: &apos;DummyPayment&apos;
                paymentMethodName:
                    type: string
                    example: &apos;Invoice&apos;
                amount:
                    type: integer
                    description: &apos;Amount in minor units (cents)&apos;
                    example: 9999
            example:
                paymentProviderName: &apos;DummyPayment&apos;
                paymentMethodName: &apos;Invoice&apos;
                amount: 9999

    quote:
        type: map
        writable: true
        readable: false
        required: true
        description: &apos;Quote context required to initialize the payment&apos;
        openapiContext:
            required: [&apos;customer&apos;, &apos;billingAddress&apos;, &apos;currency&apos;]
            properties:
                customer:
                    type: object
                    required: [&apos;firstName&apos;, &apos;lastName&apos;, &apos;email&apos;]
                    properties:
                        firstName: { type: string, example: &apos;Sonia&apos; }
                        lastName: { type: string, example: &apos;Wagner&apos; }
                        email: { type: string, format: email, example: &apos;sonia@acme.com&apos; }
                billingAddress:
                    type: object
                    required: [&apos;iso2Code&apos;]
                    properties:
                        iso2Code: { type: string, example: &apos;DE&apos; }
                currency:
                    type: object
                    required: [&apos;code&apos;]
                    properties:
                        code: { type: string, example: &apos;EUR&apos; }
            example:
                customer:
                    firstName: &apos;Sonia&apos;
                    lastName: &apos;Wagner&apos;
                    email: &apos;sonia@acme.com&apos;
                billingAddress:
                    iso2Code: &apos;DE&apos;
                currency:
                    code: &apos;EUR&apos;

    preOrderPaymentData:
        type: map
        writable: false
        readable: true
        required: false
        description: &apos;PSP-specific response payload returned by the payment provider&apos;
        openapiContext:
            example:
                transactionId: &apos;tx_abc123&apos;
                redirectUrl: &apos;https://psp.example.com/pay/tx_abc123&apos;
```

### Read-only versus write-only payloads

- **Write-only request payloads** (`writable: true`, `readable: false`) should declare
  `properties`, `required`, and `example`. The first two drive request validation and the
  generated OpenAPI schema; `example` makes the Swagger UI &quot;Try Out&quot; body usable without
  edits.
- **Read-only response payloads** (`writable: false`, `readable: true`) only need
  `openapiContext.example` when the response shape is dynamic. If the response shape is fixed,
  prefer declaring `properties` (and optionally `required`) so consumers see the full schema.

### Validation note

`openapiContext.required` controls only the OpenAPI documentation. If a request field must be
enforced at runtime, add the matching constraint to the resource&apos;s validation schema — see
[Validation schemas](/docs/integrations/spryker-api/api-platform/validation-schemas.html).

## Automatic JSON:API request body examples

For JSON:API endpoints (`application/vnd.api+json`), the generator automatically wraps property-level examples in the JSON:API envelope (`data.type` + `data.attributes`) when it builds the OpenAPI request body. You define examples once per property; the generator assembles the envelope for every write operation.

Given:

```yaml
resource:
  name: Customers
  shortName: customers   # becomes the JSON:API &quot;type&quot; field

  properties:
    email:
      type: string
      writable: true
      openapiContext:
        example: &quot;john@example.com&quot;
    firstName:
      type: string
      writable: true
      openapiContext:
        example: &quot;John&quot;
    idCustomer:
      type: integer
      writable: false      # excluded from request body example
      openapiContext:
        example: 42
```

…the generated OpenAPI request body for `POST`, `PATCH`, and `PUT` operations is:

```json
{
  &quot;data&quot;: {
    &quot;type&quot;: &quot;customers&quot;,
    &quot;attributes&quot;: {
      &quot;email&quot;: &quot;john@example.com&quot;,
      &quot;firstName&quot;: &quot;John&quot;
    }
  }
}
```

Rules the generator applies:

- The `shortName` value becomes the `type` field.
- Only **writable** properties are included — anything marked `writable: false` is filtered out (so identifiers and timestamps do not appear in the request example).
- Properties without an `openapiContext.example` are omitted from the example body.
- If no writable property has an example, no `requestBody` example is emitted at all — the operation appears without a prefilled &quot;Try Out&quot; body.

If you need a custom request body example that does not match this shape, override it at the operation level — see [Operations](#operations).

## Operations

Define which HTTP operations are available for the resource:

```yaml
operations:
  - type: Get                      # GET /customers/{id}
  - type: GetCollection            # GET /customers
  - type: Post                     # POST /customers
  - type: Put                      # PUT /customers/{id}
  - type: Patch                    # PATCH /customers/{id}
  - type: Delete                   # DELETE /customers/{id}
```

The operation names map to HTTP methods:
- `post` → POST (create)
- `get` → GET (single resource)
- `getCollection` → GET (collection)
- `put` → PUT (replace)
- `patch` → PATCH (update)
- `delete` → DELETE (remove)

## Pagination

API Platform provides built-in pagination for collection endpoints (`GetCollection`). You can configure pagination behavior per resource using YAML schema options.

### Pagination options

| Option | Type | Description |
|--------|------|-------------|
| `paginationEnabled` | `boolean` | Enables or disables pagination for this resource. When `false`, `GetCollection` returns all results without pagination. Default: inherits from global configuration. |
| `paginationItemsPerPage` | `integer` | Number of items returned per page. Overrides the global default. |
| `paginationMaximumItemsPerPage` | `integer` | Maximum number of items a client can request per page via `itemsPerPage` query parameter. Prevents clients from requesting excessively large pages. |
| `paginationClientEnabled` | `boolean` | Allows clients to enable or disable pagination via the `pagination` query parameter (for example, `?pagination=false`). |
| `paginationClientItemsPerPage` | `boolean` | Allows clients to set the number of items per page via the `itemsPerPage` query parameter (for example, `?itemsPerPage=50`). |

The global default for `paginationItemsPerPage` is defined in the project&apos;s `api_platform.php` configuration file. To override it for a specific resource, set `paginationItemsPerPage` in the resource schema.

### Minimal pagination example

```yaml
resource:
  name: Products
  shortName: products

  paginationEnabled: true
  paginationItemsPerPage: 10

  operations:
    - type: GetCollection
```

### Full pagination example

```yaml
resource:
  name: Products
  shortName: products

  paginationEnabled: true
  paginationItemsPerPage: 20
  paginationMaximumItemsPerPage: 100
  paginationClientEnabled: true
  paginationClientItemsPerPage: true

  operations:
    - type: GetCollection
    - type: Get
```

With this configuration, clients can use the following query parameters:

```bash
# Default pagination (20 items per page)
GET /products

# Navigate to page 3
GET /products?page=3

# Request 50 items per page (up to maximum of 100)
GET /products?itemsPerPage=50

# Disable pagination to get all results
GET /products?pagination=false
```

### Generated output

The pagination options are rendered as named parameters in the `#[ApiResource]` attribute:

```php
#[ApiResource(
    operations: [new GetCollection(), new Get()],
    shortName: &apos;products&apos;,
    provider: ProductsBackendProvider::class,
    paginationItemsPerPage: 20,
    paginationEnabled: true,
    paginationMaximumItemsPerPage: 100,
    paginationClientEnabled: true,
    paginationClientItemsPerPage: true
)]
```

### Provider requirements

For pagination to work, your Provider must return a `TraversablePaginator` instance for collection operations:

```php
use ApiPlatform\State\Pagination\TraversablePaginator;

return new TraversablePaginator(
    new \ArrayObject($resources),
    $currentPage,
    $itemsPerPage,
    $totalItems
);
```

If `paginationEnabled` is `true` but the Provider returns a plain array, API Platform wraps the result in a `PartialPaginatorInterface`, which may not include total count or page metadata.

### Global pagination defaults

Global pagination defaults can be configured in the application configuration file. Per-resource settings override the global defaults. See [API Platform configuration](/docs/integrations/spryker-api/api-platform/configuration.html) for details.

## Relationships

Define relationships between resources to enable including related resources via the `?include=` query parameter.

### includes section

Declares what relationships this resource can include. `includes` is declared once on the parent resource — the child resource does not need a reverse declaration.

```yaml
includes:
  - relationshipName: addresses
    targetResource: CustomersAddresses
    uriVariableMappings:
      customerReference: customerReference
```

**Entry fields:**

| Field | Required | Description |
|-------|----------|-------------|
| `relationshipName` | Yes | Name used in the `?include=` parameter and as the JSON:API relationship key. |
| `targetResource` | Yes | The `name` of the included resource as declared in its `resource.yml` (for example, `CustomersAddresses`). Also determines the JSON:API `type` field of the related resources. |
| `uriVariableMappings` | Conditional | Maps properties from the parent resource to the URI variables of the included resource. Required when the included resource is routed by URI variables. Format: `parentProperty: childUriVariable`. Ignored when `resolverClass` is set. |
| `uriTemplate` | Optional | Explicit URI template for the included resource when it has multiple operations and the relationship must target a specific path (for example, `/abstract-products/{abstractProductSku}/abstract-product-prices`). |
| `resolverClass` | Optional | Fully qualified class name of a relationship resolver. Use when the relationship cannot be expressed via URI variables — the resolver receives the parent resources and the request context, and returns the related resources directly. When `resolverClass` is set, `uriVariableMappings` and `uriTemplate` are not used for routing. See [Custom relationship resolvers](/docs/integrations/spryker-api/api-platform/relationships.html#custom-relationship-resolvers). |
| `autoInclude` | Optional | Resolve this relationship for every response of the parent type, even when the client did not request it via `?include=`. Use `autoIncludeMaxDepth` and `autoIncludeMinDepth` to bound where in the response graph the auto-include applies. |

#### URI-variable mapping example

For relationships routed by sub-resource URLs, map parent properties to child URI variables:

```yaml
includes:
  - relationshipName: abstract-product-prices
    targetResource: AbstractProductPrices
    uriTemplate: /abstract-products/{abstractProductSku}/abstract-product-prices
    uriVariableMappings:
      sku: abstractProductSku
```

#### Resolver-based example

For relationships whose targets cannot be derived from URI variables (for example, derived from order state or aggregated across multiple sources), reference a resolver class:

```yaml
includes:
  - relationshipName: order-shipments
    targetResource: OrderShipments
    resolverClass: Spryker\Glue\ShipmentsRestApi\Api\Storefront\Relationship\OrderShipmentsRelationshipResolver
```

**Further reading:** [Resource relationships](/docs/integrations/spryker-api/api-platform/relationships.html) — full reference for declaring, resolving, and troubleshooting relationships between API Platform resources, including provider-based and resolver-based dispatch, response shape, validation, and worked examples.

## Sort priority for included resources

The JSON:API response wraps related resources in an `included` array. By default, API Platform sorts that array alphabetically by resource `type`. Use `includedSortPriority` on a resource to override where its entries appear relative to other types.

### How it works

| Rule | Behavior |
|------|----------|
| Default | Every resource has an implicit priority of `0`. |
| Higher priority | Entries appear **later** in the `included` array. |
| Equal priority | Entries are sorted alphabetically by `type`. |

The priority is read from the resource&apos;s own `.resource.yml` and applied globally to every response that surfaces that type in `included`.

### Syntax

```yaml
resource:
  name: CartItems
  shortName: items

  includedSortPriority: 100
```

The generator passes the value through to the generated `#[ApiResource]` attribute via `extraProperties`:

```php
#[ApiResource(
    shortName: &apos;items&apos;,
    extraProperties: [&apos;includedSortPriority&apos; =&gt; 100],
    // ...
)]
```

### When to set a custom priority

Set `includedSortPriority` higher than `0` when a resource must appear after its nested children in the `included` array. The typical case is cart-item-like resources whose `?include=` chain resolves to abstract or concrete products: keeping the parent items last preserves the ordering of the legacy REST API and matches the order most clients expect when iterating the `included` array.

The following resources ship with `includedSortPriority: 100`:

- `items`
- `guest-cart-items`
- `bundle-items`
- `configurable-bundle-template-image-sets`

All other shipped resources rely on the default of `0`. Override the priority on project-level resources only when you need to enforce a specific ordering in `included`.

{% info_block infoBox &quot;Sort priority is not a guarantee of stable ordering across versions&quot; %}

`includedSortPriority` is a hint for the sort algorithm, not a JSON:API contract. Clients should still address resources by `type` and `id` rather than by index in the `included` array.

{% endinfo_block %}

## Resource generation process

### Generation workflow

The resource generation process is organized into distinct phases, each producing result objects for comprehensive error tracking and reporting:

```MARKDOWN
1. Preparation Phase
   ↓
2. Schema Parsing Phase → ParseResult
   - Load validation schemas
   - Parse validation rules
   - Load resource schemas
   - Parse resource definitions
   ↓
3. Schema Merging Phase → MergeResult
   - Merge schemas (Core → Feature → Project)
   - Track contributing source files
   ↓
4. Validation Phase → ValidationResult
   - Validate merged schemas
   - Apply validation rules
   ↓
5. Code Generation Phase
   - Generate PHP resource classes
   - Write files to output directory
   ↓
6. Cache Update
```

### Result objects

Each phase produces result objects that encapsulate both successful outcomes and failures:

- **ParseResult**: Contains grouped schemas and tracks failed validation files and schema files that could not be parsed
- **MergeResult**: Contains successfully merged schemas and tracks resources that failed to merge
- **ValidationResult**: Contains validated schemas and tracks resources that failed validation with detailed error messages

This structured approach ensures that errors in one resource do not block the generation of other valid resources, and provides clear feedback about what succeeded and what failed.

### Extending an existing resource (schema layering)

Spryker automatically merges schemas from multiple layers:

**Core layer** (lowest priority):

**vendor/spryker/customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      type: string
    firstName:
      type: string
```

**Feature layer** (medium priority):

**src/SprykerFeature/CRM/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    phone:
      type: string      # Added property
```

**Project layer** (highest priority):

**src/Pyz/Glue/Customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      required: true    # Override core definition
    customField:
      type: string      # Project-specific field
```

**Merged result:**

```yaml
resource:
  name: Customers
  properties:
    email:
      type: string
      required: true    # From project layer
    firstName:
      type: string      # From core layer
    phone:
      type: string      # From feature layer
    customField:
      type: string      # From project layer
```

### Generated resource class

The generator creates a complete PHP class with API Platform attributes:

```php
&lt;?php

declare(strict_types=1);
namespace Generated\Api\Backend;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiProperty;
use Symfony\Component\Validator\Constraints as Assert;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;

#[ApiResource(
    operations: [new Post(), new Get(), new GetCollection(), new Patch(), new Delete()],
    shortName: &apos;customers&apos;,
    provider: CustomerBackendProvider::class,
    processor: CustomerBackendProcessor::class,
    paginationItemsPerPage: 10,
    paginationEnabled: true,
    paginationMaximumItemsPerPage: 100,
    paginationClientEnabled: true,
    paginationClientItemsPerPage: true
)]
final class CustomersBackendResource
{
    #[ApiProperty(writable: false)]
    public ?int $idCustomer = null;

    #[ApiProperty(openapiContext: [&apos;example&apos; =&gt; &apos;john@example.com&apos;])]
    #[Assert\NotBlank(groups: [&apos;customers:create&apos;])]
    #[Assert\Email(groups: [&apos;customers:create&apos;])]
    public ?string $email = null;

    #[ApiProperty(identifier: true, writable: false)]
    public ?string $customerReference = null;

    public ?bool $isActive = true;

    // Getters, setters, toArray(), fromArray() methods...
}
```

## Debugging schemas

### Debug commands

```bash
# 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 customers --api-type=backend

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

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

# Validate schemas without generating
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only
```

### Common schema errors

The generator validates schemas and provides detailed error messages:

```bash
# Missing required fields
Error: Resource &quot;customers&quot; is missing required field &quot;name&quot;

# Invalid operation type
Error: Invalid operation type &quot;INVALID&quot;. Must be one of: Get, Post, Put, Patch, Delete, GetCollection

# Invalid property type
Error: Property &quot;age&quot; has invalid type &quot;int&quot;. Must be one of: string, integer, number, boolean, array, object

# Provider class not found
Error: Provider class &quot;Pyz\Glue\Customer\Api\Backend\Provider\MissingProvider&quot; does not exist
```

## Advanced schema features

### Custom URL paths

Operations support `uriTemplate` and `uriVariables` to define custom URL paths, including sub-resource URLs like `/customers/{customerReference}/addresses`.

#### Sub-resource with full CRUD

Define a child resource with nested URLs by adding `uriTemplate` and `uriVariables` to each operation:

**customers-addresses.resource.yml**

```yaml
resource:
  name: CustomersAddresses
  shortName: customers-addresses

  operations:
    - type: GetCollection
      uriTemplate: &apos;/customers/{customerReference}/addresses&apos;
      uriVariables:
        customerReference:
          toProperty: &apos;customer&apos;
          fromClass: CustomersStorefrontResource

    - type: Get
      uriTemplate: &apos;/customers/{customerReference}/addresses/{uuid}&apos;
      uriVariables:
        customerReference:
          toProperty: &apos;customer&apos;
          fromClass: CustomersStorefrontResource
        uuid:
          fromClass: CustomersAddressesStorefrontResource

    - type: Post
      uriTemplate: &apos;/customers/{customerReference}/addresses&apos;
      uriVariables:
        customerReference:
          toProperty: &apos;customer&apos;
          fromClass: CustomersStorefrontResource
```

**`uriVariables` properties:**
- `fromClass`: The generated resource class the variable originates from
- `toProperty`: The property on the current resource that links to the parent resource

#### Action-style sub-resource

For single-action endpoints nested under a parent resource:

**customers-confirm-registration.resource.yml**

```yaml
resource:
  name: CustomersConfirmRegistration
  shortName: customers-confirm-registration

  operations:
    - type: Post
      uriTemplate: /customers/{customerReference}/confirm-registration
```

For more details on `uriTemplate`, `uriVariables`, and sub-resource patterns, see the [API Platform sub-resources documentation](https://api-platform.com/docs/core/subresources/).

### Security expressions

Security expressions protect resources and operations using [Symfony&apos;s ExpressionLanguage](https://symfony.com/doc/current/security/expressions.html). They require the SecurityBundle to be configured. See [Integrate API Platform security](/docs/integrations/spryker-api/authenticating-and-authorization/integrate-api-platform-security.html) for setup instructions.

{% info_block infoBox &quot;Where roles come from&quot; %}

Roles like `ROLE_CUSTOMER` in security expressions come from OAuth scopes that are automatically mapped to Symfony roles. The mapping convention is as follows: a scope name is uppercased and prefixed with `ROLE_`. For example, the `customer` scope becomes `ROLE_CUSTOMER`.

Scopes are provided by scope provider plugins registered in `OauthDependencyProvider::getScopeProviderPlugins()`. The following table lists the out-of-the-box scope provider plugins and the scopes they provide:

| Plugin | Scopes |
|--------|--------|
| `CustomerOauthScopeProviderPlugin` | `customer` |
| `CompanyUserOauthScopeProviderPlugin` | `company_user` |
| `AgentOauthScopeProviderPlugin` | `agent` |
| `CustomerImpersonationOauthScopeProviderPlugin` | `customer_impersonation`, `customer` |
| `UserOauthScopeProviderPlugin` | `user`, plus UserType sub-plugins |
| `WarehouseOauthScopeProviderPlugin` | `warehouse` |

For details on how the mapping works, see [Security — Roles and OAuth scope mapping](/docs/integrations/spryker-api/authenticating-and-authorization/security.html). For instructions on setting up scopes, see [Integrate the authorization scopes](/docs/integrations/spryker-api/backend-api/integrate-backend-api/integrate-the-authorization-scopes.html).

{% endinfo_block %}

Three types of security expressions are supported:

| Expression | Evaluated | Use case | When to use |
|-----------|-----------|----------|-------------|
| `security` | Before the request is processed | Check user roles or authentication status | For role or authentication checks that do not depend on the request body. |
| `securityPostDenormalize` | After the request body is deserialized | Check authorization based on submitted data | When authorization depends on the deserialized resource `object`, for example, to verify the user owns the resource being modified. |
| `securityPostValidation` | After validation passes | Check authorization based on validated data | When authorization depends on validated data, for example, to verify a value is within the user&apos;s authorized limit after validation confirms the data is structurally correct. |

#### Resource-level security

Applies to all operations on the resource:

```yaml
resource:
  name: Customers
  shortName: customers
  security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;
```

#### Operation-level security

Applies to a specific operation, overriding resource-level security:

```yaml
resource:
  name: Customers
  shortName: customers

  operations:
    - type: Post
      # No security — public registration

    - type: Get
      security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;

    - type: Patch
      security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;
```

#### Post-denormalize security

Evaluated after the request body has been deserialized. The `object` variable contains the resource instance:

```yaml
resource:
  name: Orders
  shortName: orders
  security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;
  securityPostDenormalize: &quot;is_granted(&apos;EDIT&apos;, object)&quot;
```

{% info_block infoBox &quot;Custom voter attributes&quot; %}

`EDIT` in the example is a **custom voter attribute** — it is an application-defined string, not a built-in Symfony or Spryker constant. For `is_granted(&apos;EDIT&apos;, object)` to work, you must register a custom Symfony [Voter](https://symfony.com/doc/current/security/voters.html) that supports the `EDIT` attribute and implements the authorization logic, for example, checking that the authenticated user owns the resource.

Use `securityPostDenormalize` when the authorization decision depends on the **submitted request data** (the deserialized `object`), such as verifying resource ownership.

{% endinfo_block %}

#### Post-validation security

Evaluated after validation has passed:

```yaml
resource:
  name: Payments
  shortName: payments
  securityPostValidation: &quot;is_granted(&apos;PROCESS&apos;, object)&quot;
```

{% info_block infoBox &quot;Custom voter attributes&quot; %}

`PROCESS` in the example is a **custom voter attribute** — it is an application-defined string, not a built-in Symfony or Spryker constant. For `is_granted(&apos;PROCESS&apos;, object)` to work, you must register a custom Symfony [Voter](https://symfony.com/doc/current/security/voters.html) that supports the `PROCESS` attribute.

Use `securityPostValidation` when the authorization decision depends on **validated data**, for example, to verify a payment amount is within the user&apos;s authorized limit after validation confirms the data is structurally correct.

{% endinfo_block %}

For detailed information about the authentication flow, role mapping, and accessing the authenticated user in providers, see [Security](/docs/integrations/spryker-api/authenticating-and-authorization/security.html).

## Generation commands

### Basic generation

```bash
# Generate all configured API types
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate

# Generate specific API type
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate backend
docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue api:generate storefront

# Generate with options
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --dry-run           # Preview without writing
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only     # Only validate schemas
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --resource=customers  # Generate single resource
```

### Output

```bash
Generating API resources for ApiType: backend

Discovering schema files...
Validating schemas... OK
Merging schemas... OK

Generating resources:
 10/10 [============================] 100%

Generated: 10 file(s)
Cache updated

Done!
```

## Schema validation rules

The generator enforces these rules:

### Required fields

Every resource must have:
- `name` - Internal resource name
- `shortName` - URL-friendly name
- At least one `operation`
- At least one `property`

### Valid operation types

Only these operation types are allowed:
- `Get` - Retrieve single resource
- `GetCollection` - Retrieve collection
- `Post` - Create resource
- `Put` - Replace entire resource
- `Patch` - Update partial resource
- `Delete` - Delete resource

### Valid property types

Only these property types are allowed:
- `string`
- `integer`
- `number`
- `boolean`
- `array`
- `object`
- `map`
- `mixed`

### Provider/Processor validation

- Provider/Processor classes must exist
- Classes must implement correct interfaces
- Namespaces must be valid PHP namespaces

## Best practices

### 1. Use semantic naming

```yaml
# ✅ Good
resource:
  name: Customers              # PascalCase plural — used for schema merging
  shortName: customers         # lowercase kebab-case plural — JSON:API type + URL segment

# ✅ Good — multi-word
resource:
  name: AbstractProductPrices
  shortName: abstract-product-prices

# ❌ Bad — wrong shortName casing/form
resource:
  name: Customers
  shortName: Customer          # Should be lowercase plural

# ❌ Bad — abbreviated, unclear
resource:
  name: CustomerData
  shortName: cust
```

### 2. Document all properties

```yaml
# ✅ Good
email:
  type: string
  description: &quot;The customer&apos;s email address used for login and notifications&quot;

# ❌ Bad
email:
  type: string
```

### 3. Leverage schema merging

Core — define base properties:

**src/Spryker/Customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      type: string
```

Project — only override what is needed:

**src/Pyz/Glue/Customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      required: true  # ← Only the difference
```

### 4. Use readable/writable correctly

```yaml
# Read-only fields (IDs, timestamps)
idCustomer:
  type: integer
  writable: false

# Write-only fields (passwords)
password:
  type: string
  readable: false

# Read-write fields (normal data)
email:
  type: string
  writable: true
  readable: true
```

## Next steps

- [API Platform](/docs/integrations/spryker-api/api-platform/api-platform.html) - Architecture overview
- [Validation schemas](/docs/integrations/spryker-api/api-platform/validation-schemas.html) - Define validation rules
- [CodeBucket support](/docs/integrations/spryker-api/api-platform/code-buckets.html) - Code Bucket-specific resources
- [Implement an API Platform resource](/docs/integrations/spryker-api/api-platform/enablement.html) - Creating resources
- [Test API Platform resources](/docs/integrations/spryker-api/api-platform/testing.html) - Writing and running tests
- [Troubleshooting](/docs/integrations/spryker-api/api-platform/troubleshooting.html) - Common issues
- [API Platform Documentation](https://api-platform.com/docs/) - Official API Platform docs
</description>
            <pubDate>Tue, 11 Aug 2026 12:00:31 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/resource-schemas.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/resource-schemas.html</guid>
            
            
        </item>
        
        <item>
            <title>Resource relationships</title>
            <description>The API Platform relationship system enables resources to include related resources via the `?include=` query parameter in JSON:API format.

## Quick start

### 1. Define the relationship on the parent resource

Add an `includes` section to the parent resource YAML — this is the single source of truth for the relationship. The child resource does not declare anything relationship-specific.

```yaml
# src/Spryker/Customer/resources/api/storefront/customers.resource.yml
resource:
  name: Customers
  shortName: customers

  includes:
    - relationshipName: addresses
      targetResource: CustomersAddresses
      uriVariableMappings:
        customerReference: customerReference
```

The child resource (`CustomersAddresses` in this example) only has to exist as a resource the generator can locate by `targetResource` name. No reverse declaration is required on the child YAML.

### 2. Regenerate container

```bash
docker/sdk testing -x GLUE_APPLICATION=GLUE_STOREFRONT glue cache:clear
```

### 3. Use relationships

```bash
# Single include
GET /customers/customer--35?include=addresses

# Multiple includes
GET /customers/customer--35?include=addresses,orders
```

## Configuration reference

### includes section

Declares what relationships this resource can include. `includes` lives only on the parent resource — there is no reverse declaration on the child.

**Required properties:**
- `relationshipName`: Name used in `?include=` parameter (for example, `addresses`)
- `targetResource`: Name of the resource to include (for example, `CustomersAddresses`)

**Optional properties:**
- `uriVariableMappings`: Maps properties from parent to child provider URI variables.
- `resolverClass`: Fully qualified class name of a custom relationship resolver — see [Custom relationship resolvers](#custom-relationship-resolvers). When set, `uriVariableMappings` is ignored.
- `autoInclude`: Resolve this relationship automatically for every response of the parent type, even when the client did not request it. `autoIncludeMaxDepth` and `autoIncludeMinDepth` bound where in the response graph the auto-include applies.
- `uriTemplate`: Explicit URI template for the relationship link in the JSON:API response. Auto-generated from `targetResource` if not set.

**Example:**

```yaml
includes:
  - relationshipName: addresses
    targetResource: CustomersAddresses
    uriVariableMappings:
      customerReference: customerReference
```

## URI variable mapping

URI variable mapping passes context from parent resource to child provider.

**Example flow:**

1. Parent resource (Customer) has property `customerReference = &apos;DE--123&apos;`
2. Configuration maps `customerReference: customerReference`
3. Child provider receives `[&apos;customerReference&apos; =&gt; &apos;DE--123&apos;]` in URI variables
4. Child provider uses this to filter results

**Multiple mappings:**

```yaml
uriVariableMappings:
  customerReference: customerReference
  storeId: storeId
  locale: locale
```

## Custom relationship resolvers

Use a custom resolver when the related resource lives on a transfer property of the parent (so no extra provider call is needed), when each parent has its own distinct related resources, or when you need DI access to clients or plugins that the provider path cannot offer. For straightforward foreign-key style relationships, prefer `uriVariableMappings` plus the child provider — it is simpler and benefits from request-scoped caching automatically.

### YAML configuration

Reference the resolver class on the parent&apos;s `includes` entry:

```yaml
includes:
  - relationshipName: vouchers
    targetResource: Vouchers
    resolverClass: Spryker\Glue\CartCodesRestApi\Api\Storefront\Relationship\CartsVouchersRelationshipResolver
```

`targetResource` is still required — it determines the JSON:API `type` field of the related resources. `uriVariableMappings` is ignored when `resolverClass` is set; the resolver is fully responsible for producing the related resources.

### Interfaces

Resolvers implement one of two interfaces from `Spryker\ApiPlatform\Relationship`:

| Interface | Method | Returns | Use when |
|-----------|--------|---------|----------|
| `RelationshipResolverInterface` | `resolve(array $parentResources, array $context): array&lt;object&gt;` | A flat list of related resources, attached to all parents in the response. | The set of related resources is the same for every parent, or there is only one parent. |
| `PerItemRelationshipResolverInterface` (extends the above) | `resolvePerItem(array $parentResources, array $context): array&lt;string, array&lt;object&gt;&gt;` | A map of `parentIdentifier =&gt; relatedResources`. The framework deduplicates by IRI before attaching, so a resource referenced from multiple parents appears once in `included`. | Each parent has its own distinct related resources, for example each `company-user` row has a different `customer`, `company`, and `business-unit`. |

### Base class

`Spryker\ApiPlatform\Relationship\AbstractRelationshipResolver` provides a starting point with request-scoped helpers — use it when implementing `RelationshipResolverInterface`:

| Helper | Returns |
|--------|---------|
| `getParentResources()` | The parent resources passed to `resolve()`. |
| `getRequest()` / `hasRequest()` | The current Symfony `Request`. |
| `getLocale()` / `hasLocale()` | `LocaleTransfer` from request attributes. |
| `getStore()` / `hasStore()` | `StoreTransfer` from request attributes. |
| `getCustomer()` / `hasCustomer()` | `CustomerTransfer` from request attributes. |
| `getCustomerReference()` | Shortcut for `getCustomer()-&gt;getCustomerReferenceOrFail()`. |

Subclasses implement `resolveRelationship(): array&lt;object&gt;`.

### Worked example: basic resolver

This resolver expands a transfer property (`voucherDiscounts`) on a parent `Carts` resource into a list of `Vouchers` storefront resources. The parent already carries the data, so no extra provider call is needed.

```php
&lt;?php

namespace Spryker\Glue\CartCodesRestApi\Api\Storefront\Relationship;

use Generated\Api\Storefront\VouchersStorefrontResource;
use Generated\Shared\Transfer\DiscountTransfer;
use Spryker\ApiPlatform\Relationship\AbstractRelationshipResolver;
use Spryker\Service\Serializer\SerializerServiceInterface;

class CartsVouchersRelationshipResolver extends AbstractRelationshipResolver
{
    public function __construct(protected SerializerServiceInterface $serializer)
    {
    }

    /**
     * @return array&lt;VouchersStorefrontResource&gt;
     */
    protected function resolveRelationship(): array
    {
        $resources = [];

        foreach ($this-&gt;getParentResources() as $parent) {
            foreach ($parent-&gt;voucherDiscounts ?? [] as $discountTransfer) {
                if (!$discountTransfer instanceof DiscountTransfer) {
                    continue;
                }

                $resources[] = $this-&gt;serializer-&gt;denormalize(
                    $discountTransfer-&gt;toArray(),
                    VouchersStorefrontResource::class,
                );
            }
        }

        return $resources;
    }
}
```

### Worked example: per-item resolver

This resolver fetches a different `CompanyUsers` resource per parent. The framework attaches each parent&apos;s record only to that parent and deduplicates the `included` block by IRI.

```php
&lt;?php

namespace Spryker\Glue\CompanyUsersRestApi\Api\Storefront\Relationship;

use Generated\Api\Storefront\CompanyUsersStorefrontResource;
use Generated\Shared\Transfer\CompanyUserTransfer;
use Spryker\ApiPlatform\Relationship\PerItemRelationshipResolverInterface;
use Spryker\Client\CompanyUser\CompanyUserClientInterface;
use Spryker\Service\Serializer\SerializerServiceInterface;

class CompanyUsersRelationshipResolver implements PerItemRelationshipResolverInterface
{
    public function __construct(
        protected CompanyUserClientInterface $companyUserClient,
        protected SerializerServiceInterface $serializer,
    ) {
    }

    /**
     * @return array&lt;CompanyUsersStorefrontResource&gt;
     */
    public function resolve(array $parentResources, array $context): array
    {
        $all = [];

        foreach ($this-&gt;resolvePerItem($parentResources, $context) as $resources) {
            $all = array_merge($all, $resources);
        }

        return $all;
    }

    /**
     * @return array&lt;string, array&lt;CompanyUsersStorefrontResource&gt;&gt;
     */
    public function resolvePerItem(array $parentResources, array $context): array
    {
        $result = [];

        foreach ($parentResources as $parent) {
            $uuid = $parent-&gt;companyUserUuid ?? null;

            if ($uuid === null) {
                continue;
            }

            $transfer = $this-&gt;companyUserClient-&gt;findCompanyUserByUuid($uuid);

            $result[$uuid] = $transfer instanceof CompanyUserTransfer
                ? [$this-&gt;serializer-&gt;denormalize($transfer-&gt;toArray(), CompanyUsersStorefrontResource::class)]
                : [];
        }

        return $result;
    }
}
```

### Dependency injection

The compiler pass registers each `resolverClass` automatically as a public, autowired, autoconfigured service. You do not need to declare the resolver in `services.yaml`.

- Typed constructor parameters are autowired — for example, `SerializerServiceInterface` or any `*ClientInterface`.
- Inject plugin stacks from a DependencyProvider via the `#[Plugins]` attribute on a constructor parameter:

  ```php
  use Spryker\Service\Container\Attributes\Plugins;

  public function __construct(
      #[Plugins(dependencyProviderMethod: &apos;getDiscountMapperPlugins&apos;)]
      protected array $discountMapperPlugins = [],
  ) {
  }
  ```

{% info_block warningBox &quot;Glue collaborators&quot; %}

Glue resolvers may inject Client interfaces (`*ClientInterface`) only. They must not inject Zed facade interfaces (`*FacadeInterface`); cross the Glue/Zed boundary via a client.

{% endinfo_block %}

### Resolution semantics

- **Caching:** the framework calls the resolver once per unique parent-resource set per request. The cache key combines the resolver class with the parent object identity hashes.
- **Per-item deduplication:** `PerItemRelationshipResolverInterface` results are deduplicated by IRI before being attached, so a resource referenced from multiple parents appears once in `included`.
- **`?include=` flattening:** nested includes are auto-expanded. `?include=addresses.country` resolves both `addresses` and `addresses.country` without each having to be listed explicitly.
- **Auto-include:** add `autoInclude: true` on the parent&apos;s `includes` entry to resolve the relationship for every response of that parent type, even when the client did not request it. Use `autoIncludeMaxDepth` and `autoIncludeMinDepth` to scope where in the response graph the auto-include applies. This is the mechanism used to fold `concrete-products` automatically under `bundled-products`:

  ```yaml
  includes:
    - relationshipName: concrete-products
      targetResource: ConcreteProducts
      uriTemplate: /concrete-products/{sku}
      uriVariableMappings:
        sku: sku
      autoInclude: true
  ```

### Failure modes

- **Class is not autoloadable.** `RelationshipConfigurationPass` writes a container log warning and silently drops the relationship. Run `composer dump-autoload` and check that the PSR-4 namespace matches the file path.
- **Class does not implement the interface.** The dispatcher returns an empty list with no error. Confirm the class implements `RelationshipResolverInterface` (or `PerItemRelationshipResolverInterface`).
- **Resolver throws.** The exception is not caught by the dispatcher — wrap external calls inside the resolver and return `[]` on expected absence rather than letting the exception bubble through.

For tests, treat the resolver as a regular autowired service: unit-test by constructing it directly with stubs, or integration-test the full include path through Codeception API tests. See [Test API Platform resources](/docs/integrations/spryker-api/api-platform/testing.html).

## Auto-generated properties

When you define an `includes` relationship, the corresponding property is automatically generated with these defaults:

| Attribute | Value | Rationale |
|-----------|-------|-----------|
| `type` | `array` | Relationships are collections |
| `writable` | `false` | Relationships are read-only |
| `readable` | `true` | Must be readable for responses |
| `required` | `false` | Relationships are optional |
| `description` | `&quot;Related {targetResource} resources&quot;` | Auto-generated description |

You can override defaults by manually defining the property:

```yaml
properties:
  addresses:
    type: array
    writable: false
    readable: true
    required: false
    description: &quot;Customer billing and shipping addresses&quot;
```

{% info_block warningBox &quot;A relationship property cannot also declare an items block&quot; %}

When you override a relationship property, do not add an `items` block to it. Schema validation rejects
a property that is both a relationship target and an inline object list.

Only one docblock is emitted per property, and a relationship property already receives one describing
its target resource. An `items` block would compete for the same slot, so the ambiguity is rejected
rather than silently resolved. A property is either a relationship to another resource or an inline
object list — not both. See
[Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html).

{% endinfo_block %}

## Validation

Relationship configuration is checked in two places, with different behaviour.

**Structural validation (`RelationshipValidationRule`, during code generation).** Checks that each `includes` entry is an array, that `relationshipName` and `targetResource` are present and string-typed, and that `uriVariableMappings` (if set) is an array. Failures surface as warnings on the generator output.

```text
Warning: includes[0] is missing required field &quot;targetResource&quot; in src/Spryker/Customer/resources/api/storefront/customers.resource.yml
```

**Resource resolution (`RelationshipConfigurationPass`, during container compile).** Resolves each include to a target resource provider — or to a `resolverClass` when one is set. There is no resource-existence error here: an unknown `targetResource`, or a `resolverClass` that is not autoloadable, causes the relationship to be **silently dropped** from the registry. The resolver-class case writes a container log entry; the missing-target case does not.

If a relationship returns nothing at runtime and no warning was emitted by the generator, suspect a silent drop: confirm `targetResource` matches the target resource&apos;s `name` or `shortName`, and that the target&apos;s schema file lives in a scanned source directory.

## Response format

**Request:**

```http
GET /customers/customer--35?include=addresses
```

**Response:**

```json
{
  &quot;data&quot;: {
    &quot;type&quot;: &quot;customers&quot;,
    &quot;id&quot;: &quot;customer--35&quot;,
    &quot;attributes&quot;: {
      &quot;email&quot;: &quot;john@example.com&quot;,
      &quot;firstName&quot;: &quot;John&quot;
    },
    &quot;relationships&quot;: {
      &quot;addresses&quot;: {
        &quot;data&quot;: [
          {&quot;type&quot;: &quot;addresses&quot;, &quot;id&quot;: &quot;addr-123&quot;},
          {&quot;type&quot;: &quot;addresses&quot;, &quot;id&quot;: &quot;addr-456&quot;}
        ]
      }
    }
  },
  &quot;included&quot;: [
    {
      &quot;type&quot;: &quot;addresses&quot;,
      &quot;id&quot;: &quot;addr-123&quot;,
      &quot;attributes&quot;: {
        &quot;address1&quot;: &quot;123 Test St&quot;,
        &quot;city&quot;: &quot;Test City&quot;
      }
    },
    {
      &quot;type&quot;: &quot;addresses&quot;,
      &quot;id&quot;: &quot;addr-456&quot;,
      &quot;attributes&quot;: {
        &quot;address1&quot;: &quot;456 Other St&quot;,
        &quot;city&quot;: &quot;Other City&quot;
      }
    }
  ]
}
```

### Ordering of the `included` array

Entries in `included` are sorted by the `includedSortPriority` of each resource type — higher priority appears later, and entries with the same priority are sorted alphabetically by `type`. The default priority is `0`. To override it for a project resource, see [Sort priority for included resources](/docs/integrations/spryker-api/api-platform/resource-schemas.html#sort-priority-for-included-resources).

## How it works

1. **RelationshipProviderDecorator** wraps all providers automatically.
2. Parses `?include=` parameter from request.
3. **ApiPlatformRelationshipResolver** loads relationships via container configuration.
4. **Dispatch:** for relationships configured with `resolverClass`, the resolver is fetched from the container and `resolve()` (or `resolvePerItem()`) is called with the parent resources and request context. Otherwise URI variables are mapped from the parent and passed to the child provider.
5. **JsonApiRelationshipNormalizer** builds the JSON:API response with `relationships` and `included` sections.

Providers require no code changes — the system works automatically through decoration.

## Performance

Relationships are resolved per parent resource. For a collection of N parent resources with an `?include=` request, the child provider is called N times — one call per parent — which can produce an N+1 query pattern if the child provider hits the database per call.

When you expect collection endpoints to be requested with `?include=`, optimize the child provider:

- **Batch internally**: have the child provider detect repeated single-key lookups and coalesce them into one underlying query. For example, accept a `customerReference` URI variable but maintain an in-request cache of previously fetched results.
- **Paginate the parent**: keep parent collection page sizes small (`paginationItemsPerPage`) so the per-include cost stays bounded.
- **Profile real traffic**: enable Doctrine query logging or use Blackfire/Xdebug to confirm the N+1 hypothesis before optimizing — sometimes the parent&apos;s own query dominates and the includes are negligible.

## Troubleshooting

### Relationships are not returned

The `?include=` parameter is silently ignored or returns no `relationships` block.

Run through the following checks in order:

1. **Clear the cache.** Relationship configuration is built into the compiled container; YAML changes do not take effect until the container is rebuilt.

    ```bash
    docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue cache:clear
    ```

2. **Confirm the relationship is declared on the parent.** The parent resource YAML must carry an `includes` entry whose `targetResource` exactly matches the child resource `name`. The child resource needs no reverse declaration — see the [Configuration reference](#configuration-reference).

3. **Inspect the compiled relationship registry.** API Platform exposes the merged configuration as a container parameter:

    ```bash
    docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue debug:container --parameter=api_platform.relationships
    ```

    The output lists every registered relationship keyed by `{parentResource}.{relationshipName}` (for example, `customers.addresses`). If your relationship is missing, the YAML was not picked up — re-check file location and run `api:generate`.

4. **Verify the child provider is registered.** The child resource needs a provider that API Platform can resolve:

    ```bash
    docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue debug:container | grep &lt;ChildProviderClass&gt;
    ```

### `relationships` block is present but `data` is empty

The relationship is wired up but no related resources come back.

1. **The child provider is returning `null` or `[]`.** Call the child provider directly (or hit its standalone collection endpoint with the same URI variable values) to confirm it returns data.
2. **URI variable mapping does not produce a value.** A property on the parent that resolves to `null` is omitted from the URI variables passed to the child — verify the mapped property is populated on every parent resource in the response. Use `api:debug &lt;resource&gt; --show-merged` to confirm the property is declared.
3. **The child filters too aggressively.** Inspect the child provider&apos;s filtering logic with the URI variable values produced by the mapping.

### Invalid include names are ignored

Unknown values in `?include=` (for example, a typo or a relationship the parent does not declare) are silently dropped — the response succeeds without that relationship and no error is raised. If a deployment appears to lose a relationship after a release, suspect a typo or a missing `includes:` entry on the parent resource before assuming a runtime failure.

### Custom resolver is not invoked

The relationship is configured with `resolverClass` but the resolver does not run and no related resources appear.

1. **Confirm the class is autoloadable.** Run `composer dump-autoload` and verify the PSR-4 namespace matches the file path. When the class cannot be loaded, `RelationshipConfigurationPass` writes a container log warning and silently drops the relationship from the registry.
2. **Inspect the compiled relationship registry.** A resolver-backed relationship shows `resolver_class` in the merged configuration:

    ```bash
    docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue debug:container --parameter=api_platform.relationships
    ```

    If the entry is missing or has no `resolver_class`, the include was dropped during container compilation.
3. **Verify the interface is implemented.** The dispatcher calls `resolve()` only when the class implements `RelationshipResolverInterface` (or `PerItemRelationshipResolverInterface`). A class that does not implement either interface returns an empty list with no error.
4. **Check for exceptions inside the resolver.** Throws bubble out of the dispatcher — wrap external calls inside the resolver and return `[]` on expected absence rather than letting the exception propagate.
</description>
            <pubDate>Tue, 11 Aug 2026 12:00:31 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/relationships.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/relationships.html</guid>
            
            
        </item>
        
        <item>
            <title>API Platform</title>
            <description>&lt;p&gt;Spryker’s API Platform integration provides schema-based API resource generation with automatic OpenAPI documentation. This allows you to define your API resources using YAML schemas and automatically generate fully functional API endpoints with validation, pagination, and &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/serialization.html&quot;&gt;serialization&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This document describes the API Platform architecture and how it integrates with Spryker.&lt;/p&gt;
&lt;h2 id=&quot;what-is-api-platform&quot;&gt;What is API Platform&lt;/h2&gt;
&lt;p&gt;API Platform is a framework for building modern APIs based on web standards and best practices. In Spryker, it complements the existing Glue API infrastructure by providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Schema-based resource generation&lt;/strong&gt;: Define resources in YAML, generate PHP classes automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic OpenAPI documentation&lt;/strong&gt;: Interactive API documentation generated from schemas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in validation&lt;/strong&gt;: Symfony Validator integration with operation-specific rules&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination support&lt;/strong&gt;: Standardized pagination with configurable defaults&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State management&lt;/strong&gt;: Separate providers (read) and processors (write) for clean architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read more about the API Platform project at &lt;a href=&quot;https://api-platform.com/&quot;&gt;api-platform.com&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-spryker-is-moving-to-api-platform&quot;&gt;Why Spryker is moving to API Platform&lt;/h3&gt;
&lt;p&gt;API Platform replaces Spryker-specific patterns for routing, authentication, and resource definition with industry-standard Symfony conventions, automatic OpenAPI schema generation, and a clean separation between resource schema, provider, and validation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Previous infrastructure&lt;/th&gt;
&lt;th&gt;API Platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bootstrap&lt;/td&gt;
&lt;td&gt;Spryker-specific application bootstrap&lt;/td&gt;
&lt;td&gt;Symfony Kernel-based routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource registration&lt;/td&gt;
&lt;td&gt;Manual plugin registration in &lt;code&gt;GlueApplicationDependencyProvider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Declarative YAML resource definitions (&lt;code&gt;*.resource.yml&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Custom flows per module&lt;/td&gt;
&lt;td&gt;Standard OAuth2 / Symfony Security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coupling&lt;/td&gt;
&lt;td&gt;Tight coupling between resource and routing logic&lt;/td&gt;
&lt;td&gt;Clean separation: provider + resource schema + validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;Complex to test and extend&lt;/td&gt;
&lt;td&gt;Symfony-native, testable with standard PHPUnit patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAPI&lt;/td&gt;
&lt;td&gt;Manual / partial&lt;/td&gt;
&lt;td&gt;Automatic OpenAPI schema generation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;architecture-overview&quot;&gt;Architecture overview&lt;/h2&gt;
&lt;h3 id=&quot;resource-generation-workflow&quot;&gt;Resource generation workflow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-MARKDOWN&quot;&gt;&gt;Schema Files (YAML)
    ↓
Schema Discovery &amp;amp; Validation
    ↓
Multi-layer Schema Merging (Core → Feature → Project → [Code Buckets])
    ↓
Resource Class Generation
    ↓
API Platform Resource (with attributes)
    ↓
API Endpoints
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;core-components&quot;&gt;Core components&lt;/h3&gt;
&lt;h4 id=&quot;schema-files&quot;&gt;1. Schema files&lt;/h4&gt;
&lt;p&gt;Resources are defined in YAML files located in module directories:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-MARKDOWN&quot;&gt;&gt;src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.resource.yml
src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.validation.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example resource schema &lt;code&gt;src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.resource.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;shortName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;API&quot;&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Pyz&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Glue&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Api&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Backend&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Provider&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CustomerBackendProvider&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;processor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Pyz&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Glue&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Api&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Backend&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Processor&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CustomerBackendProcessor&quot;&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;paginationEnabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;operations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Post&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Get&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GetCollection&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Patch&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Delete&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;address&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;customerReference&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;identifier&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;writable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Example validation schema &lt;code&gt;src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.validation.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;NotBlank&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name is required&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Length&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;64&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;minMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name must be at least 2 characters&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;maxMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name cannot exceed 64 characters&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;constraints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Length&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;64&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;minMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name must be at least 2 characters&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;maxMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name cannot exceed 64 characters&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;generated-resources&quot;&gt;2. Generated resources&lt;/h4&gt;
&lt;p&gt;The generator creates PHP classes with API Platform attributes:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/Generated/Api/Backend/CustomersBackendResource.php&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Generated\Api\Backend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApiPlatform\Metadata\ApiResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApiPlatform\Metadata\ApiProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\Validator\Constraints&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;#[ApiResource(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;operations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;GetCollection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()],&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shortName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;customers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProvider&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;processor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProcessor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomersBackendResource&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[ApiProperty(identifier: true, writable: false)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerReference&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;#[ApiProperty]&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[Assert\NotBlank(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[Assert\Email(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Getters, setters, toArray(), fromArray()...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;state-providers-and-processors&quot;&gt;3. State providers and processors&lt;/h4&gt;
&lt;p&gt;Detailed information about the API-Platform Provider and Resources can be found on the public docs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://api-platform.com/docs/core/state-providers/&quot;&gt;API Platform Providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://api-platform.com/docs/core/state-processors/&quot;&gt;API Platform Processors&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Provider (read operations):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProviderInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;provide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Operation&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$uriVariables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Fetch and return data from your business layer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Processor (write operations):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProcessor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProcessorInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Operation&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$uriVariables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Persist changes through your business layer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$updatedResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;api-types&quot;&gt;API types&lt;/h2&gt;
&lt;p&gt;Any of the &lt;a href=&quot;/docs/integrations/spryker-api/getting-started-with-apis/getting-started-with-apis.html&quot;&gt;existing APIs&lt;/a&gt; can be extended using API Platform.&lt;/p&gt;
&lt;p&gt;Spryker supports multiple API types for different use cases:&lt;/p&gt;
&lt;h3 id=&quot;glue-api&quot;&gt;Glue API&lt;/h3&gt;
&lt;p&gt;This API is configured to serve the &lt;a href=&quot;https://jsonapi.org/format/&quot;&gt;JSON:API&lt;/a&gt; format by default; to change the supported formats, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/configuration.html#configure-supported-formats&quot;&gt;Configure supported formats&lt;/a&gt;. Projects migrating their APIs can provide new APIs as well as supporting the existing ones while migrating.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;storefront&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; Glue&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://glue.eu.spryker.local/&lt;/code&gt; - Configurable per project&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Customer-facing APIs, mobile apps, PWAs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;gluestorefront-api&quot;&gt;GlueStorefront API&lt;/h3&gt;
&lt;p&gt;This API serves the &lt;a href=&quot;https://jsonapi.org/format/&quot;&gt;JSON:API&lt;/a&gt; format by default; additional formats, such as JSON-LD, can be enabled per project. For instructions, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/configuration.html#configure-supported-formats&quot;&gt;Configure supported formats&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;storefront&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; GlueStorefront&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://glue-storefront.eu.spryker.local/&lt;/code&gt; - Configurable per project&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Customer-facing APIs, mobile apps, PWAs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;gluebackend-api&quot;&gt;GlueBackend API&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;backend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; GlueBackend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://glue-backend.eu.spryker.local/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Admin panels, internal tools, ERP integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;merchant-portal-api&quot;&gt;Merchant Portal API&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;merchant-portal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; MerchantPortal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://mp.glue.eu.spryker.local/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Marketplace merchant interfaces&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example:&lt;/strong&gt; &lt;code&gt;/products&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;multi-layer-schema-merging&quot;&gt;Multi-layer schema merging&lt;/h2&gt;
&lt;p&gt;One of the key features is support for multi-layer schema definitions that automatically merge:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core layer&lt;/strong&gt; (vendor/spryker):&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Feature layer&lt;/strong&gt; (src/SprykerFeature):&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;loyaltyPoints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Project layer&lt;/strong&gt; (src/Pyz):&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Override core&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;customField&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# Project-specific&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: A single merged resource with all properties, project code-bucket layer taking precedence.&lt;/p&gt;
&lt;h2 id=&quot;integration-with-spryker-architecture&quot;&gt;Integration with Spryker architecture&lt;/h2&gt;
&lt;h3 id=&quot;dependency-injection&quot;&gt;Dependency Injection&lt;/h3&gt;
&lt;p&gt;API Platform fully integrates with Symfony Dependency Injection:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// config/Zed/ApplicationServices.php&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$services&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Pyz\\Zed\\&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;../../../src/Pyz/Zed/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Providers and Processors are automatically discovered and can use constructor injection:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProviderInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CustomerFacadeInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CustomerRepositoryInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;facade-integration&quot;&gt;Facade integration&lt;/h3&gt;
&lt;p&gt;Resources can leverage existing Spryker facades:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProcessor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProcessorInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CustomerFacadeInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Operation&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$customerTransfer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapToTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;customerFacade&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createCustomer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$customerTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapToResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getCustomerTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;resource-generation&quot;&gt;Resource generation&lt;/h2&gt;
&lt;h3 id=&quot;console-commands&quot;&gt;Console commands&lt;/h3&gt;
&lt;p&gt;All the following commands can be used with a specific GLUE_APPLICATION by prefixing them with &lt;code&gt;GLUE_APPLICATION=GLUE_BACKEND&lt;/code&gt; environment variable. For example: &lt;code&gt;docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug --list&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Generate resource classes for all configured API types at once. Usually used during deployment/installation.&lt;/span&gt;
docker/sdk cli glue api:generate

&lt;span class=&quot;c&quot;&gt;# Generate API type specific resource classes. Usually used during development.&lt;/span&gt;
docker/sdk cli glue api:generate backend

&lt;span class=&quot;c&quot;&gt;# Validate schemas only to see if there is any issue in the definitions&lt;/span&gt;
docker/sdk cli glue api:generate &lt;span class=&quot;nt&quot;&gt;--validate-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;debug-commands&quot;&gt;Debug commands&lt;/h3&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# List all resources to see which ones are defined in the schema files.&lt;/span&gt;
docker/sdk cli glue  api:debug &lt;span class=&quot;nt&quot;&gt;--list&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Inspect specific resource and print details about properties and operations&lt;/span&gt;
docker/sdk cli glue  api:debug customers &lt;span class=&quot;nt&quot;&gt;--api-type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;backend

&lt;span class=&quot;c&quot;&gt;# Show merged schema&lt;/span&gt;
docker/sdk cli glue  api:debug customers &lt;span class=&quot;nt&quot;&gt;--api-type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;backend &lt;span class=&quot;nt&quot;&gt;--show-merged&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Show contributing files for a resource&lt;/span&gt;
docker/sdk cli glue  api:debug customers &lt;span class=&quot;nt&quot;&gt;--api-type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;backend &lt;span class=&quot;nt&quot;&gt;--show-sources&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
&lt;h3 id=&quot;automatic-openapi-documentation&quot;&gt;Automatic OpenAPI documentation&lt;/h3&gt;
&lt;p&gt;API Platform generates interactive OpenAPI documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Swagger UI at the root URL &lt;code&gt;/&lt;/code&gt; for example &lt;code&gt;http://glue-backend.eu.spryker.local/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can disable this interface in production environments by configuring the settings in your &lt;code&gt;api_platform.php&lt;/code&gt; configuration file. For details, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/configuration.html#enable-the-documentation-ui-only-in-development&quot;&gt;Enable the documentation UI only in development&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;built-in-validation&quot;&gt;Built-in validation&lt;/h3&gt;
&lt;p&gt;Validation rules from &lt;code&gt;*.validation.yml&lt;/code&gt; files are converted to Symfony Validator constraints:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;NotBlank&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#[Assert\NotBlank(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#[Assert\Email(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;pagination-support&quot;&gt;Pagination support&lt;/h3&gt;
&lt;p&gt;Standardized pagination with query parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-MARKDOWN&quot;&gt;&gt;GET /customers?page=2&amp;amp;itemsPerPage=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Provider returns &lt;code&gt;PaginatorInterface&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TraversablePaginator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ArrayObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$currentPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$itemsPerPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$totalItems&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;operation-specific-behavior&quot;&gt;Operation-specific behavior&lt;/h3&gt;
&lt;p&gt;Define different validation and behavior per operation:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;operations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Post&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# Create&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Get&lt;/span&gt;             &lt;span class=&quot;c1&quot;&gt;# Read one&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GetCollection&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# Read many&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Patch&lt;/span&gt;           &lt;span class=&quot;c1&quot;&gt;# Update&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Delete&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;# Delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Each operation can have specific validation rules and security settings.&lt;/p&gt;
&lt;h3 id=&quot;relationships&quot;&gt;Relationships&lt;/h3&gt;
&lt;p&gt;Include related resources via the &lt;code&gt;?include=&lt;/code&gt; query parameter:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;relationshipName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;addresses&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;targetResource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CustomersAddresses&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;uriVariableMappings&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;customerReference&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;customerReference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Request:&lt;/p&gt;
&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /customers/customer--35?include=addresses
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Response includes both the customer and related addresses in JSON:API format. No provider code changes required - relationships work automatically through decoration.&lt;/p&gt;
&lt;p&gt;For detailed information, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/relationships.html&quot;&gt;Resource relationships&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;sparse-fieldsets&quot;&gt;Sparse fieldsets&lt;/h3&gt;
&lt;p&gt;Request only the attributes you need using the &lt;code&gt;fields&lt;/code&gt; query parameter:&lt;/p&gt;
&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /stores?fields[stores]=name,locale
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This returns only &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;locale&lt;/code&gt; in the response attributes, reducing payload size. Sparse fieldsets work with relationships too — filter attributes on both the main resource and included resources.&lt;/p&gt;
&lt;p&gt;For detailed information, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/sparse-fieldsets.html&quot;&gt;Sparse Fieldsets&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;performance&quot;&gt;Performance&lt;/h2&gt;
&lt;h3 id=&quot;opcache&quot;&gt;Opcache&lt;/h3&gt;
&lt;p&gt;API Platform loads a significantly larger class graph per request than the legacy Glue stack—the Symfony kernel, serializer, validator, security components, and the generated resource classes. Opcache must be enabled on all deployed environments; without it, every request recompiles this class graph, adding a flat overhead of seconds per request. For configuration details, see &lt;a href=&quot;/docs/dg/dev/guidelines/performance-guidelines/general-performance-guidelines.html#opcache-activation&quot;&gt;Opcache activation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;cache-warming&quot;&gt;Cache warming&lt;/h3&gt;
&lt;p&gt;API Platform deployment requires two sequential steps, not alternatives:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate the API resource classes from the schema files:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker/sdk cli glue api:generate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Warm the application cache—including the &lt;strong&gt;router cache&lt;/strong&gt;—once the resources from step 1 exist. Run it per Glue application:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker/sdk cli &lt;span class=&quot;nv&quot;&gt;GLUE_APPLICATION&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;GLUE_STOREFRONT glue cache:warmup
docker/sdk cli &lt;span class=&quot;nv&quot;&gt;GLUE_APPLICATION&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;GLUE_BACKEND glue cache:warmup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;API Platform registers its operations as routes in the standard Symfony router, whose compiled matcher and generator are dumped to &lt;code&gt;data/cache/Glue&amp;lt;Storefront|Backend&amp;gt;/&amp;lt;environment&amp;gt;/url_matching_routes.php&lt;/code&gt; and &lt;code&gt;url_generating_routes.php&lt;/code&gt;. &lt;code&gt;cache:warmup&lt;/code&gt; builds these dumps from the resource collection produced in step 1. Add both steps to your deployment and installation recipes for every API Platform application.&lt;/p&gt;
&lt;section class=&apos;info-block info-block--warning&apos;&gt;&lt;i class=&apos;info-block__icon icon-warning&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Use cache:warmup, not api:router:cache:warm-up&lt;/div&gt;
&lt;p&gt;&lt;code&gt;api:router:cache:warm-up&lt;/code&gt; warms only the legacy Glue (&lt;code&gt;GlueApplication&lt;/code&gt;) custom-route router—it does &lt;strong&gt;not&lt;/strong&gt; build the API Platform router dump. Use &lt;code&gt;cache:warmup&lt;/code&gt; (or &lt;code&gt;cache:clear&lt;/code&gt;) to warm the API Platform router.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h4 id=&quot;multi-container-and-cloud-deployments&quot;&gt;Multi-container and cloud deployments&lt;/h4&gt;
&lt;p&gt;In production, applications run with debug disabled. The router dump is then written once and never revalidated—whatever route set it was first built from is frozen for the life of the container.&lt;/p&gt;
&lt;p&gt;In a single-container setup this is harmless: the cache is warmed in the same place that serves requests, with the full route set. In a multi-container topology where resource generation runs in a build container and requests are served by a separate runtime container (for example, AWS ECS), you must guarantee the router dump is built against the complete resource collection &lt;strong&gt;for the runtime container&lt;/strong&gt;—either warmed in the runtime container after deployment, or baked at build time only if &lt;code&gt;data/cache&lt;/code&gt; is shipped to every runtime replica with the full route set.&lt;/p&gt;
&lt;p&gt;If the dump is built before resources are generated (an empty or incomplete collection), the runtime container freezes that empty dump and:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every API request returns HTTP 404 (Glue code &lt;code&gt;007&lt;/code&gt;, legacy fallthrough) because the route is absent from the matcher;&lt;/li&gt;
&lt;li&gt;once the matcher is partially rebuilt, data endpoints return HTTP 500 from IRI generation (&lt;code&gt;RouteNotFoundException&lt;/code&gt;), because the URL generator dump is also empty;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/docs.json&lt;/code&gt; returns 0 paths, even though &lt;code&gt;api:debug --list&lt;/code&gt; shows the resources resolving correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To recover a frozen container, clear and re-warm the cache (&lt;code&gt;cache:clear&lt;/code&gt;) with the full resource collection present.&lt;/p&gt;
&lt;h3 id=&quot;property-level-access-control&quot;&gt;Property-level access control&lt;/h3&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;writable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# Can be written&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;readable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Not in responses&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;comparison-with-glue-api&quot;&gt;Comparison with Glue API&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;API Platform&lt;/th&gt;
&lt;th&gt;Glue API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Definition&lt;/td&gt;
&lt;td&gt;Schema-based (YAML)&lt;/td&gt;
&lt;td&gt;Code-based (PHP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;Auto-generated OpenAPI&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Declarative&lt;/td&gt;
&lt;td&gt;Programmatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standards&lt;/td&gt;
&lt;td&gt;JSON:API by default; JSON-LD and other formats available&lt;/td&gt;
&lt;td&gt;JSON API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use cases&lt;/td&gt;
&lt;td&gt;Standard CRUD&lt;/td&gt;
&lt;td&gt;Complex business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both can coexist in the same application. For further migration guidance, see &lt;a href=&quot;/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/migrate-to-api-platform.html&quot;&gt;Migrate to API Platform&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;next-steps&quot;&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/integrate-api-platform.html&quot;&gt;Integrate API Platform&lt;/a&gt; - Setup and configuration&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/authenticating-and-authorization/integrate-api-platform-security.html&quot;&gt;Integrate API Platform security&lt;/a&gt; - Authentication and authorization setup&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/migrate-to-api-platform.html&quot;&gt;Migrate to API Platform&lt;/a&gt; - Migrate endpoints from Glue API&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/configuration.html&quot;&gt;API Platform configuration&lt;/a&gt; - Configure API Platform settings&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/authenticating-and-authorization/security.html&quot;&gt;Security&lt;/a&gt; - Authentication and authorization&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/enablement.html&quot;&gt;Implement an API Platform resource&lt;/a&gt; - Creating your first resource&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/resource-schemas.html&quot;&gt;Resource schemas&lt;/a&gt; - Resource schemas&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/typed-collections.html&quot;&gt;Typed collections in the published contract&lt;/a&gt; - What object collections publish, and when to adopt them&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/validation-schemas.html&quot;&gt;Validation schemas&lt;/a&gt; - Validation schemas&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/native-api-platform-resources.html&quot;&gt;Native API Platform resources&lt;/a&gt; - Using native PHP attributes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/code-buckets.html&quot;&gt;CodeBucket support&lt;/a&gt; - Region-specific resources&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/sparse-fieldsets.html&quot;&gt;Sparse Fieldsets&lt;/a&gt; - Request only needed attributes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/serialization.html&quot;&gt;Serialization&lt;/a&gt; - How requests and responses are serialized&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/integrations/spryker-api/api-platform/troubleshooting.html&quot;&gt;Troubleshooting API Platform&lt;/a&gt; - Common issues&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://api-platform.com/docs/&quot;&gt;API Platform official documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
            <pubDate>Tue, 11 Aug 2026 12:00:31 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/api-platform.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/api-platform.html</guid>
            
            
        </item>
        
        <item>
            <title>ESLint and Prettier in the Cypress boilerplate</title>
            <description>&lt;p&gt;ESLint and Prettier help maintain code quality and consistency in the cypress-boilerplate by enforcing coding standards and formatting rules.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESLint&lt;/strong&gt; is a static code analysis tool that identifies and fixes problems in code. It enforces coding standards and helps catch syntax errors, potential bugs, and other problematic patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prettier&lt;/strong&gt; is an opinionated code formatter that ensures a consistent code style by automatically formatting your code. It supports multiple languages and integrates well with various editors and tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both tools are already integrated into the boilerplate. The boilerplate keeps its own &lt;code&gt;package.json&lt;/code&gt;, so ESLint, Prettier, and their plugins are installed inside the boilerplate directory and are independent of any linting the surrounding project performs.&lt;/p&gt;
&lt;h2 id=&quot;eslint&quot;&gt;ESLint&lt;/h2&gt;
&lt;h3 id=&quot;configuration&quot;&gt;Configuration&lt;/h3&gt;
&lt;p&gt;ESLint uses the flat configuration format and is configured through &lt;code&gt;eslint.config.js&lt;/code&gt;. There is no &lt;code&gt;.eslintrc&lt;/code&gt; file, and ignore patterns live in the configuration itself rather than in an &lt;code&gt;.eslintignore&lt;/code&gt; file.&lt;/p&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ignores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;node_modules&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;dist&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.envs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// TypeScript parser and plugin for .ts files&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;**/*.ts&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;languageOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;parser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@typescript-eslint/parser&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;parserOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./tsconfig.json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;tsconfigRootDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;__dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ecmaVersion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2023&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;sourceType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@typescript-eslint&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@typescript-eslint/eslint-plugin&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Cypress plugin for the test files&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cypress/**/*.{js,ts}&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;cypress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;eslint-plugin-cypress&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Basic JS handling&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;**/*.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;languageOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;ecmaVersion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2023&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;sourceType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;The configuration is an array of blocks. Each block applies to the files matched by its &lt;code&gt;files&lt;/code&gt; pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ignores&lt;/code&gt;: paths ESLint never looks at.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;files&lt;/code&gt;: the glob the block applies to.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languageOptions.parser&lt;/code&gt;: &lt;code&gt;@typescript-eslint/parser&lt;/code&gt;, so TypeScript can be parsed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languageOptions.parserOptions.project&lt;/code&gt;: points at &lt;code&gt;tsconfig.json&lt;/code&gt;, which enables type-aware linting. A file that is not covered by &lt;code&gt;tsconfig.json&lt;/code&gt; produces a parsing error rather than being skipped.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plugins&lt;/code&gt;: makes a plugin’s rules available under a namespace, for example, &lt;code&gt;@typescript-eslint&lt;/code&gt; or &lt;code&gt;cypress&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rules&lt;/code&gt;: the rules that are actually enforced.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;enabling-rules&quot;&gt;Enabling rules&lt;/h3&gt;
&lt;p&gt;Flat configuration has no implicit &lt;code&gt;extends&lt;/code&gt;: registering a plugin makes its rules available under a namespace, but does not turn any of them on. Rules are enforced only where they are listed in a &lt;code&gt;rules&lt;/code&gt; block or brought in from a shared configuration.&lt;/p&gt;
&lt;p&gt;To enforce a recommended set, spread it into the exported array and add your own rules after it:&lt;/p&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tseslint&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;typescript-eslint&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pluginCypress&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;eslint-plugin-cypress/flat&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;eslintConfigPrettier&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;eslint-config-prettier&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tseslint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;configs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;recommended&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;pluginCypress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;configs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;recommended&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;eslintConfigPrettier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@typescript-eslint/no-inferrable-types&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@typescript-eslint/explicit-function-return-type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;off&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@typescript-eslint/no-explicit-any&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;off&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Order matters: later entries override earlier ones, so put &lt;code&gt;eslint-config-prettier&lt;/code&gt; last to switch off formatting rules that would otherwise conflict with Prettier.&lt;/p&gt;
&lt;h3 id=&quot;running-eslint&quot;&gt;Running ESLint&lt;/h3&gt;
&lt;p&gt;Run from the boilerplate directory:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm run lint:check
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This runs &lt;code&gt;eslint .&lt;/code&gt;, checks your code for linting errors, and displays them in the terminal.&lt;/p&gt;
&lt;h3 id=&quot;automatically-fixing-eslint-errors&quot;&gt;Automatically fixing ESLint errors&lt;/h3&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;eslint &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;prettier&quot;&gt;Prettier&lt;/h2&gt;
&lt;h3 id=&quot;configuration-1&quot;&gt;Configuration&lt;/h3&gt;
&lt;p&gt;Prettier is configured through the &lt;code&gt;.prettierrc.json&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;semi&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tabWidth&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;useTabs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;singleQuote&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;trailingComma&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;es5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;bracketSpacing&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;arrowParens&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;always&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;endOfLine&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;auto&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;semi&lt;/code&gt;: &lt;code&gt;false&lt;/code&gt; disables semicolons, except in a few scenarios.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tabWidth&lt;/code&gt;: &lt;code&gt;2&lt;/code&gt; sets two spaces per indentation level.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useTabs&lt;/code&gt;: &lt;code&gt;false&lt;/code&gt; uses spaces instead of tabs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;singleQuote&lt;/code&gt;: &lt;code&gt;true&lt;/code&gt; uses single quotes instead of double quotes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trailingComma&lt;/code&gt;: &lt;code&gt;es5&lt;/code&gt; prints trailing commas wherever possible in ES5, such as in objects and arrays.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bracketSpacing&lt;/code&gt;: &lt;code&gt;true&lt;/code&gt; prints spaces between brackets in object literals.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arrowParens&lt;/code&gt;: &lt;code&gt;always&lt;/code&gt; always includes parentheses around arrow function parameters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;endOfLine&lt;/code&gt;: &lt;code&gt;auto&lt;/code&gt; maintains existing line endings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prettier resolves the nearest configuration file for each file it formats. Because this configuration sits inside the boilerplate directory, these rules apply to the boilerplate even when Prettier is invoked from a parent project that has its own, different configuration.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.prettierignore&lt;/code&gt; file excludes paths from formatting:&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;node_modules
workflows
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;running-prettier&quot;&gt;Running Prettier&lt;/h3&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm run prettier:check
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This runs &lt;code&gt;prettier . --check&lt;/code&gt;, checks your code for style errors, and displays them in the terminal.&lt;/p&gt;
&lt;h3 id=&quot;automatically-fixing-prettier-errors&quot;&gt;Automatically fixing Prettier errors&lt;/h3&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;prettier &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This formats your code according to the rules in &lt;code&gt;.prettierrc.json&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;running-both-checks&quot;&gt;Running both checks&lt;/h2&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm run code:check   &lt;span class=&quot;c&quot;&gt;# report ESLint and Prettier issues&lt;/span&gt;
npm run code:fix     &lt;span class=&quot;c&quot;&gt;# fix both&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;section class=&apos;info-block info-block--warning&apos;&gt;&lt;i class=&apos;info-block__icon icon-warning&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Do not use code:check as a pass/fail gate&lt;/div&gt;
&lt;p&gt;&lt;code&gt;code:check&lt;/code&gt; is defined as &lt;code&gt;eslint . ; prettier . --check&lt;/code&gt;. The &lt;code&gt;;&lt;/code&gt; means the script exits with &lt;code&gt;Prettier&apos;s&lt;/code&gt; status, so a failing ESLint run is reported as success. Use it to view all issues at once, but gate on the two commands separately:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm run lint:check &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run prettier:check
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This is why CI runs them as two separate steps.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://eslint.org/docs/latest/use/configure/configuration-files&quot;&gt;ESLint flat configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prettier.io/docs/en/options.html&quot;&gt;Prettier configuration options&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
            <pubDate>Mon, 10 Aug 2026 07:04:25 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/guidelines/testing-guidelines/cypress-testing/eslint-and-prettier.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/guidelines/testing-guidelines/cypress-testing/eslint-and-prettier.html</guid>
            
            
        </item>
        
        <item>
            <title>Integrate Symfony Messenger</title>
            <description>This document describes how to integrate and configure the Symfony Messenger module into your Spryker project.

## Description

Symfony Messenger is a component that lets you dispatch and handle messages using different transports. By integrating Symfony Messenger into your Spryker project, you can switch between RabbitMQ and other transports, such as SQS, Redis, and even a database, for queue handling. You can also use Symfony Messenger for other use cases that require synchronous or asynchronous message processing.

{% info_block warningBox &quot;Check if you are using correct mode&quot; %}

Symfony Messenger module working only with Dynamic Multistore mode, so make sure that your project is using it before proceeding with the installation and configuration.

{% endinfo_block %}

## Install

{% info_block warningBox &quot;Check if you have it installed&quot; %}

Check that the following modules have been installed:

| MODULE                    | EXPECTED DIRECTORY                         |
|---------------------------|--------------------------------------------|
| SymfonyMessenger          | vendor/spryker/symfony-messenger           |
| SymfonyMessengerExtension | vendor/spryker/symfony-messenger-extension |

If so, skip this section. If not, install the missing modules before proceeding.

{% endinfo_block %}

Install the required modules using Composer:

```shell
composer require spryker/symfony-messenger
```

## Usage as a Queue Adapter

You can use Symfony Messenger as a queue adapter in Spryker to replace the existing RabbitMQ adapter.

In order to use Symfony Messenger as a queue adapter, you need to configure it and enable the corresponding plugins.

### Configure

1. Provide a DSN for the Queue transport in `config/Shared/config_default.php`:

```php
&lt;?php

use Spryker\Shared\SymfonyMessenger\SymfonyMessengerConstants;

// Symfony Messenger configuration
$config[SymfonyMessengerConstants::QUEUE_DSN] = &apos;amqp://guest:guest@localhost:5672/eu_host&apos;
];
```

Or you can build it with RabbitMQ connection details:

***config/Shared/config_default.php***

```php
foreach ($rabbitConnections as $key =&gt; $connection) {
    ...
    $config[SymfonyMessengerConstants::QUEUE_DSN] = sprintf(
        &apos;amqp://%s:%s@%s:%s/%s&apos;,
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_USERNAME],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_PASSWORD],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_HOST],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_PORT],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_VIRTUAL_HOST],
    );
}
```

The protocol in the DSN determines which transport is used. Out of the box, Spryker provides RabbitMQ as the transport for queue processing. You do not need to provide a queue name in the DSN because the application defines it when dispatching messages.

2. Provide a list of queues that can be processed.

***src/Pyz/Client/SymfonyMessenger/SymfonyMessengerConfig.php***

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerConfig extends SprykerSymfonyMessengerConfig
{
    /**
     * @return array&lt;mixed&gt;
     */
    public function getQueueConfiguration(): array
    {
        return array_merge(
            [
                EventConstants::EVENT_QUEUE =&gt; [
                    EventConfig::EVENT_ROUTING_KEY_RETRY =&gt; EventConstants::EVENT_QUEUE_RETRY,
                    EventConfig::EVENT_ROUTING_KEY_ERROR =&gt; EventConstants::EVENT_QUEUE_ERROR,
                ],
                ...
            ],
        );
    }
}
```

This configuration is similar to the RabbitMQ configuration, so you can copy it from `\Pyz\Client\RabbitMq\RabbitMqConfig::getQueueConfiguration()`.

### Enable Queue Adapter

To enable the Symfony Messenger queue adapter, register the required plugins:

1. Add the Symfony Messenger transport plugin to `src/Pyz/Zed/Queue/QueueDependencyProvider.php`:

***src/Pyz/Zed/Queue/QueueDependencyProvider.php***

```php
&lt;?php

namespace Pyz\Client\Queue;

use Spryker\Client\Kernel\Container;
use Spryker\Client\Queue\QueueDependencyProvider as BaseQueueDependencyProvider;

class QueueDependencyProvider extends BaseQueueDependencyProvider
{
    /**
     * @param \Spryker\Client\Kernel\Container $container
     *
     * @return array&lt;\Spryker\Client\Queue\Model\Adapter\AdapterInterface&gt;
     */
    protected function createQueueAdapters(Container $container): array
    {
        return [
            $container-&gt;getLocator()-&gt;rabbitMq()-&gt;client()-&gt;createQueueAdapter(),
            // You can add the adapter from the Symfony Messenger module without removing the existing one so that you can switch between them when needed.
            $container-&gt;getLocator()-&gt;symfonyMessenger()-&gt;client()-&gt;createQueueAdapter(),
        ];
    }
}
```

2. Enable adapter in `config/Shared/config_default.php`:

***config/Shared/config_default.php***

```php
&lt;?php

use Spryker\Client\SymfonyMessenger\Adapter\SymfonyMessengerQueueAdapter;

$config[QueueConstants::QUEUE_ADAPTER_CONFIGURATION] = [
    EventConstants::EVENT_QUEUE =&gt; [
        QueueConfig::CONFIG_QUEUE_ADAPTER =&gt; SymfonyMessengerQueueAdapter::class,
    ],
];

$config[QueueConstants::QUEUE_ADAPTER_CONFIGURATION_DEFAULT] = [
    QueueConfig::CONFIG_QUEUE_ADAPTER =&gt; SymfonyMessengerQueueAdapter::class,
];
```

This steps will replace the existing RabbitMQ adapter with the Symfony Messenger adapter for the queues defined in the configuration.

### Additional configuration

To provide additional configuration for the Symfony Messenger transport, use the following approach:

#### Provide queue transport configuration

You can specify transport options per queue or provide a default configuration for all queues.

The following example shows the default configuration for all queues.
Example below is a default configuration for the AMQP transport, which is used for queue processing in Symfony Messenger. You can adjust it according to your needs.

***src/Pyz/Client/SymfonyMessenger/SymfonyMessengerConfig.php***

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerConfig extends SprykerSymfonyMessengerConfig
{
    /**
     * Specification:
     * - Returns transport configuration for queue transport.
     * - Each key is a queue name, each value is an array of transport options.
     * - `default` key is used for default transport configuration.
     *
     * @api
     *
     * @return array&lt;string, array&lt;string, mixed&gt;&gt;
     */
    public function getQueueTransportConfiguration(): array
    {
        return [
            &apos;default&apos; =&gt; [
                &apos;auto_setup&apos; =&gt; false,
                &apos;persistent&apos; =&gt; &apos;true&apos;,
                &apos;connect_timeout&apos; =&gt; 3,
                &apos;read_timeout&apos; =&gt; 130,
                &apos;write_timeout&apos; =&gt; 130,
                &apos;heartbeat&apos; =&gt; 0,
                &apos;rpc_timeout&apos; =&gt; 0,
            ],
        ];
    }
}

```

{% info_block warningBox &quot;Verification&quot; %}

To verify that the Symfony Messenger Queue Adapter integration is working correctly:

1. Save any entity in the Back Office that should be synced to the storefront or run an import.
2. Check the RabbitMQ management interface to check if queues have messages and they are being processed.
3. Check that messages are being processed successfully and there are no errors in the logs.

{% endinfo_block %}

## Usage as a Message Consumer

Symfony Messenger is not limited to queue adapter usage. You can also use it as a message consumer for messages dispatched in your application. To use Symfony Messenger as a message consumer, configure it and enable the required plugins.
In order to use Symfony Messenger as a message consumer, you need to configure it and enable the corresponding plugins.

1. Install required transport factory.

Out of the box, Symfony Messenger module provides the AMQP as a transport option. If any other transport options is required it must be added separately. To do this, implement `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportFactoryProviderPluginInterface` that provides transport factories that can create a transport instance. A single plugin can provide multiple transport factories.

Example below will provide the `SchedulerTransportFactory` that allows to use Symfony Messenger for processing scheduled tasks in the Symfony Scheduler module, but you can provide any transport factory that you need.

```php
&lt;?php

namespace Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger;

class SchedulerTransportFactoryProviderPlugin extends AbstractPlugin implements TransportFactoryProviderPluginInterface
{
    /**
     * {@inheritDoc}
     * - Returns SchedulerTransportFactory instance to be used by Symfony Messenger.
     *
     * @api
     *
     * @return array&lt;\Symfony\Component\Messenger\Transport\TransportFactoryInterface&gt;
     */
    public function getTransportFactories(): array
    {
        return [
            $this-&gt;getFactory()-&gt;createSchedulerTransportFactory(),//Will return an instance of SchedulerTransportFactory that is used for processing scheduled tasks in the Symfony Scheduler module.
        ];
    }
}
```

Wire it in the dependency provider of Symfony Messenger module:


**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportFactoryProviderPluginInterface&gt;
     */
    protected function getTransportFactoryProviderPlugins(): array
    {
        return [
            new SchedulerTransportFactoryProviderPlugin(),
        ];
    }
}
```

2. Configure transports for messages

Transport factories are used to create transport instances that handle messages. Each transport is described by a name, a DSN, and an optional priority. This configuration is provided via an implementation of `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\AvailableTransportConfigProviderPluginInterface`, which returns a map of transport name to `MessengerTransportConfigTransfer`.

```php
&lt;?php

namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

use Generated\Shared\Transfer\MessengerTransportConfigTransfer;
use Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\AvailableTransportConfigProviderPluginInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

class FooBarAsyncTransportConfigProviderPlugin extends AbstractPlugin implements AvailableTransportConfigProviderPluginInterface
{
    /**
     * @return array&lt;string, \Generated\Shared\Transfer\MessengerTransportConfigTransfer&gt;
     */
    public function getTransportConfigByTransportName(): array
    {
        return [
            &apos;foo_bar_async&apos; =&gt; (new MessengerTransportConfigTransfer())
                -&gt;setDsn(&apos;amqp://guest:guest@localhost:5672/eu_host&apos;)
                -&gt;setPriority(100),
        ];
    }
}
```

The `priority` defines the transport consumption order within a worker: the higher the number, the earlier the transport is polled. When omitted, priority defaults to `0`. Make sure the DSN uses a valid transport protocol (for example `amqp://` for AMQP, `redis://` for Redis, `schedule://` for the scheduler transport).

Wire it in the dependency provider of Symfony Messenger module:

**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\AvailableTransportConfigProviderPluginInterface&gt;
     */
    protected function getAvailableTransportConfigProviderPlugins(): array
    {
        return [
            new FooBarAsyncTransportConfigProviderPlugin(),
        ];
    }
}
```

3. Map messages to transports and handlers.

Message is a data object that is dispatched via Symfony Messenger and processed by the handler. Handler is a callable that contains the logic for processing the message.
Message can be any object that can be serialized and deserialized by Symfony Messenger. It can be a transfer or any other DTO. Handler must be a callable that processes the message. It can be a class that implements the `__invoke()` method or any other callable.
Yon need to map messages to handlers and transports via `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\MessageMappingProviderPluginInterface` plugin.

First, create a message and a handler that you want to map to each other.

```php
namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

class FooBarMessage
{
    protected string $data;

    public function __construct(string $data)
    {
        $this-&gt;data = $data;
    }

    public function getData(): string
    {
        return $this-&gt;data;
    }
}
```

```php
namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

class FooBarMessageHandler
{
    public function __invoke(FooBarMessage $message): void
    {
        //Handle the message
    }
}
```

And we need to map them to each other and to the transport that will handle them:

```php
&lt;?php

namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

use Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\MessageMappingProviderPluginInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

class FooBarMappingProviderPlugin extends AbstractPlugin implements MessageMappingProviderPluginInterface
{
    public function getMessageToHandlerMap(): array
    {
        return [
            FooBarMessage::class =&gt; [
                new FooBarMessageHandler(),
            ],
        ];
    }

    public function getMessageToTransportMap(): array
    {
        return [
            FooBarMessage::class =&gt; [&apos;foo_bar_async&apos;], //DSN provided in FooBarAsyncTransportConfigProviderPlugin will be used to create a transport that will handle the message.
        ];
    }
}
```

Wire it in the dependency provider of Symfony Messenger module:

**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    protected function getMessageMappingProviderPlugins(): array
    {
        return [
            new FooBarMessageMappingProviderPlugin(),
        ];
    }
}
```

4. Send message.

To send a message, use `SymfonyMessengerClientInterface::sendMessage()`, which the module provides. The client resolves the appropriate transport and sends the message. If the transport is synchronous, it handles the message immediately and calls the corresponding handler. Otherwise, a worker processes the message from the transport.

5. Register a consumer command.

Asynchronous messages are processed by a worker that consumes messages from the transport. To run the worker, you need to register a console command that will start it.

***src/Pyz/Zed/Console/ConsoleDependencyProvider.php***

```php
&lt;?php

namespace Pyz\Zed\Console;

class ConsoleDependencyProvider extends SprykerConsoleDependencyProvider
{
    protected function getConsoleCommands(Container $container): array
    {
        return [
            new SymfonyMessengerConsumeMessagesConsole(),
        ];
    }
}
```

6. Run the worker.

```shell
console symfonymessenger:consume foo_bar_async
```

The argument is the name of the transport that you want to consume messages from. You can provide multiple transport names if you want to consume messages from different transports in one worker:

```shell
console symfonymessenger:consume foo_bar_async another_transport
```

By default, the worker will run indefinitely, but you can provide an option to stop it after a certain time in seconds:

```shell
console symfonymessenger:consume foo_bar_async --time-limit=100
```

### Consume transports in parallel

By default a single worker process polls the given transports one after another. To process a heavy workload faster, use the `--parallel` (`-p`) option to run several competing consumers. When the value is greater than `1`, the command spawns that many child processes of itself, each consuming the same transports:

```shell
console symfonymessenger:consume foo_bar_async --parallel=4
```

Each child process is a full worker; the output of every worker is streamed back to the parent, prefixed with `[worker-N]`, and stopping the parent (SIGTERM/SIGINT) gracefully stops all children.

{% info_block warningBox &quot;When parallel consumption is safe&quot; %}

Parallel consumption is only safe for transports where competing consumers do not process the same message twice — for example, AMQP work queues. The scheduler transport is also safe to run in parallel: each scheduled job is guarded by a lock that the cron jobs builder creates through the Lock client, so the same schedule is never executed by more than one worker at the same time. The exception is a job configured with `no_lock` set to `true`, which is not guarded and can therefore be executed by several parallel workers at once — only enable it for jobs that are safe to run concurrently.

{% endinfo_block %}

### Pause a transport at runtime

Sometimes a transport must be temporarily skipped by the worker without stopping the whole consumer — for example, when a scheduled job is disabled from the Back Office. Implement `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportConsumeGuardPluginInterface` to veto consumption of a transport on a per-iteration basis. Before consuming from a transport, the worker calls every guard plugin; if any of them returns `false`, that transport is skipped in the current loop iteration.

```php
&lt;?php

namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

use Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportConsumeGuardPluginInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

class FooBarTransportConsumeGuardPlugin extends AbstractPlugin implements TransportConsumeGuardPluginInterface
{
    public function canConsumeTransport(string $transportName): bool
    {
        // Return false to skip consuming this transport in the current worker iteration.
        return true;
    }
}
```

Wire it in the dependency provider of Symfony Messenger module:

**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportConsumeGuardPluginInterface&gt;
     */
    protected function getTransportConsumeGuardPlugins(): array
    {
        return [
            new FooBarTransportConsumeGuardPlugin(),
        ];
    }
}
```

The Symfony Scheduler module ships `\Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\DisabledSchedulerJobTransportGuardPlugin`, which uses this extension point to pause the transport of a scheduled job that has been disabled from the Back Office. See [Integrate Symfony Scheduler](/docs/dg/dev/integrate-and-configure/integrate-symfony-scheduler.html).

## Additional information

Because this module relies on Symfony Messenger, see the [Symfony Messenger documentation](https://symfony.com/doc/current/messenger.html) for details about configuration and usage. You can also review the module source code to understand its implementation and available features.</description>
            <pubDate>Mon, 10 Aug 2026 06:33:22 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-messenger.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-messenger.html</guid>
            
            
        </item>
        
    </channel>
</rss>
