Relationships
Edit on GitHubThe API Platform relationship system enables resources to include related resources via the ?include= query parameter in JSON:API format.
Quick start
1. Define relationships in parent resource
Add an includes section to your parent resource YAML:
# src/Spryker/Customer/resources/api/storefront/customers.resource.yml
resource:
name: Customers
shortName: customers
includes:
- relationshipName: addresses
targetResource: CustomersAddresses
uriVariableMappings:
customerReference: customerReference
2. Define reverse relationship in child resource
Add an includableIn section to your child resource YAML:
# src/Spryker/Customer/resources/api/storefront/customers-addresses.resource.yml
resource:
name: CustomersAddresses
shortName: customers-addresses
includableIn:
- resource: Customers
relationshipName: addresses
uriVariableMappings:
customerReference: customerReference
Both declarations must match for validation to pass.
3. Regenerate container
docker/sdk testing -x GLUE_APPLICATION=GLUE_STOREFRONT glue cache:clear
4. Use relationships
# 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.
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
Example:
includes:
- relationshipName: addresses
targetResource: CustomersAddresses
uriVariableMappings:
customerReference: customerReference
includableIn section
Declares where this resource can be included.
Required properties:
resource: Name of the parent resourcerelationshipName: Must match parent’s includes declaration
Optional properties:
uriVariableMappings: Must match parent’s includes declaration
Example:
includableIn:
- resource: Customers
relationshipName: addresses
uriVariableMappings:
customerReference: customerReference
URI variable mapping
URI variable mapping passes context from parent resource to child provider.
Example flow:
- Parent resource (Customer) has property
customerReference = 'DE--123' - Configuration maps
customerReference: customerReference - Child provider receives
['customerReference' => 'DE--123']in URI variables - Child provider uses this to filter results
Multiple mappings:
uriVariableMappings:
customerReference: customerReference
storeId: storeId
locale: locale
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 |
"Related {targetResource} resources" |
Auto-generated description |
You can override defaults by manually defining the property:
properties:
addresses:
type: array
writable: false
readable: true
required: false
description: "Customer billing and shipping addresses"
Validation
The system validates relationships during code generation:
Bi-directional consistency:
- Parent’s
includesmust match child’sincludableIn - Relationship names must match
- URI variable mappings must match
Resource existence:
- Target resource must exist
- Referenced properties should exist
Example error:
Validation Error in customers.resource.yml:
- includes[0].targetResource: Resource "CustomersAddresses" declares
includableIn for "Customers" but uses different relationshipName
"customerAddresses". Expected: "addresses"
Response format
Request:
GET /customers/customer--35?include=addresses
Response:
{
"data": {
"type": "customers",
"id": "customer--35",
"attributes": {
"email": "john@example.com",
"firstName": "John"
},
"relationships": {
"addresses": {
"data": [
{"type": "addresses", "id": "addr-123"},
{"type": "addresses", "id": "addr-456"}
]
}
}
},
"included": [
{
"type": "addresses",
"id": "addr-123",
"attributes": {
"address1": "123 Test St",
"city": "Test City"
}
},
{
"type": "addresses",
"id": "addr-456",
"attributes": {
"address1": "456 Other St",
"city": "Other City"
}
}
]
}
How it works
- RelationshipProviderDecorator wraps all providers automatically
- Parses
?include=parameter from request - ApiPlatformRelationshipResolver loads relationships via container configuration
- Maps URI variables from parent to child
- Calls child provider with mapped variables
- JsonApiRelationshipNormalizer builds JSON:API response with
relationshipsandincludedsections
Providers require no code changes - the system works automatically through decoration.
Custom relationship resolvers
The default provider-based resolution maps URI variables from the parent resource to the child provider. When that is not enough — for example, the related data lives on the parent’s context payload, is aggregated from several sources, or needs custom denormalization — declare a resolver class instead.
Reference the resolver in the parent’s includes entry via resolverClass. When resolverClass is set, uriVariableMappings and targetResource are not used for routing; the resolver class is invoked directly with the parent resources and request context:
# src/Spryker/OrdersRestApi/resources/api/storefront/orders.resource.yml
resource:
name: Orders
shortName: orders
includes:
- relationshipName: order-amendments
targetResource: OrderAmendments
resolverClass: Spryker\Glue\OrderAmendmentsRestApi\Api\Storefront\Relationship\OrderAmendmentsRelationshipResolver
The resolver class must implement Spryker\ApiPlatform\Relationship\RelationshipResolverInterface. In practice, extend Spryker\ApiPlatform\Relationship\AbstractRelationshipResolver, which gives you helpers for accessing the request, locale, store, and customer transfers:
namespace Spryker\Glue\OrderAmendmentsRestApi\Api\Storefront\Relationship;
use Generated\Api\Storefront\OrderAmendmentsStorefrontResource;
use Spryker\ApiPlatform\Relationship\AbstractRelationshipResolver;
use Spryker\Service\Serializer\SerializerServiceInterface;
class OrderAmendmentsRelationshipResolver extends AbstractRelationshipResolver
{
public function __construct(protected SerializerServiceInterface $serializer)
{
}
/**
* @return array<\Generated\Api\Storefront\OrderAmendmentsStorefrontResource>
*/
protected function resolveRelationship(): array
{
$resources = [];
foreach ($this->getParentResources() as $orderResource) {
$contextData = $orderResource->context ?? null;
$amendmentData = is_array($contextData) && isset($contextData['salesOrderAmendment'])
? $contextData['salesOrderAmendment']
: null;
if (!is_array($amendmentData) || $amendmentData === []) {
continue;
}
$resources[] = $this->serializer->denormalize(
$amendmentData,
OrderAmendmentsStorefrontResource::class,
);
}
return $resources;
}
}
The RelationshipConfigurationPass compiler pass registers the class as an autowired public service automatically — no manual service definition is required. If the referenced class does not exist when the container compiles, the relationship is silently skipped and a compiler log entry is emitted.
Use a custom resolver when:
- The related data is already attached to the parent (for example, embedded in a transfer’s
contextarray) and a separate child provider would re-fetch it unnecessarily. - The relationship aggregates data from several sources that no single provider exposes.
- The link from parent to child cannot be expressed as a simple property-to-URI-variable mapping.
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
customerReferenceURI 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’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:
-
Clear the cache. Relationship configuration is built into the compiled container; YAML changes do not take effect until the container is rebuilt.
docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue cache:clear -
Confirm the parent declares the relationship. Runtime resolution only reads
includeson the parent — that declaration must be present and the names/uriVariableMappingsmust match what the request uses. A matchingincludableInon the child is optional but recommended for discoverability; it does not affect runtime behavior. See the Configuration reference. -
Inspect the compiled relationship registry. API Platform exposes the merged configuration as a container parameter:
docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue debug:container --parameter=api_platform.relationshipsThe 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 runapi:generate. -
Verify the child provider is registered. The child resource needs a provider that API Platform can resolve:
docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue debug:container | grep <ChildProviderClass>
Validation error: bi-directional consistency
Resource generation fails with an error like:
Validation Error in customers.resource.yml:
- includes[0].targetResource: Resource "CustomersAddresses" declares includableIn
for "Customers" but uses different relationshipName "customerAddresses"
Expected: "addresses"
The parent’s includes[].relationshipName and the child’s includableIn[].relationshipName must be identical strings. The same applies to uriVariableMappings — every mapping declared on the parent must appear on the child with the same source/target names.
relationships block is present but data is empty
The relationship is wired up but no related resources come back.
- The child provider is returning
nullor[]. Call the child provider directly (or hit its standalone collection endpoint with the same URI variable values) to confirm it returns data. - URI variable mapping does not produce a value. A property on the parent that resolves to
nullis omitted from the URI variables passed to the child — verify the mapped property is populated on every parent resource in the response. Useapi:debug <resource> --show-mergedto confirm the property is declared. - The child filters too aggressively. Inspect the child provider’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 includableIn in the child before assuming a runtime failure.
Thank you!
For submitting the form