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 variablesresolverClass: Fully qualified class name of a custom relationship resolver (see Custom relationship resolvers)autoInclude: Whentrue, this relationship is automatically included without requiring?include=(see Auto-include)
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
For relationships that require complex data fetching logic beyond simple URI variable mapping, use a custom resolver class.
When to use resolverClass
Use resolverClass instead of uriVariableMappings when:
- The relationship requires custom data extraction or aggregation
- Related data cannot be fetched with a simple provider call
- You need access to request context (locale, store, customer) for business logic
- Data must be extracted from nested or computed properties on the parent resource
Configuration
includes:
- relationshipName: merchants
resolverClass: Spryker\Glue\Merchant\Api\Storefront\Relationship\OrderMerchantsRelationshipResolver
targetResource: merchants
When resolverClass is specified, the system skips the standard URI variable mapping and delegates relationship resolution to the resolver class. The targetResource is optional and used for type discovery in the JSON:API response.
Implementation
Extend AbstractRelationshipResolver and implement the resolveRelationship() method:
<?php
namespace Spryker\Glue\Merchant\Api\Storefront\Relationship;
use Spryker\ApiPlatform\Relationship\AbstractRelationshipResolver;
use Spryker\Zed\Merchant\Business\MerchantFacadeInterface;
class OrderMerchantsRelationshipResolver extends AbstractRelationshipResolver
{
public function __construct(
private MerchantFacadeInterface $merchantFacade,
) {
}
/**
* @return array<object>
*/
protected function resolveRelationship(): array
{
$merchantReferences = [];
foreach ($this->getParentResources() as $parentResource) {
$merchantReferences[] = $parentResource->getMerchantReference();
}
return $this->merchantFacade->getMerchantsByReferences(
array_unique(array_filter($merchantReferences)),
);
}
}
AbstractRelationshipResolver provides the following helpers:
| Method | Purpose |
|---|---|
getParentResources() |
Access the parent resource objects |
getRequest() |
Access the Symfony Request |
getLocale() |
Get the current locale |
getStore() |
Get the current store |
getCustomer() |
Get the authenticated customer |
getCustomerReference() |
Get the authenticated customer reference |
Resolver classes are automatically registered and autowired by the container.
Per-item relationship resolvers
Standard resolvers return a flat list of related resources that applies to all parent resources. Per-item resolvers return related resources scoped to each individual parent, which is necessary for one-to-one or parent-scoped relationships in collection responses.
When to use per-item resolvers
Use PerItemRelationshipResolverInterface when each parent resource has its own specific set of related resources. For example:
- Each company user references a different customer
- Each order has different order items
- Each cart has different cart items
Implementation
Implement PerItemRelationshipResolverInterface and return related resources keyed by parent identifier:
<?php
namespace Spryker\Glue\CompanyUser\Api\Storefront\Relationship;
use Spryker\ApiPlatform\Relationship\AbstractRelationshipResolver;
use Spryker\ApiPlatform\Relationship\PerItemRelationshipResolverInterface;
class CompanyUserCustomersRelationshipResolver extends AbstractRelationshipResolver implements PerItemRelationshipResolverInterface
{
/**
* @return array<object>
*/
protected function resolveRelationship(): array
{
return array_merge(...array_values($this->resolvePerItem($this->getParentResources(), [])));
}
/**
* @return array<string, array<object>>
*/
public function resolvePerItem(array $parentResources, array $context): array
{
$result = [];
foreach ($parentResources as $parentResource) {
$parentId = (string) $parentResource->getCompanyUserUuid();
$customer = $this->findCustomerForCompanyUser($parentResource);
if ($customer !== null) {
$result[$parentId] = [$customer];
}
}
return $result;
}
}
The resolvePerItem() method returns an associative array where:
- Keys are parent resource identifiers (as strings)
- Values are arrays of related resource objects for that specific parent
Nested and flat includes
The relationship system supports both nested and flat include syntax for backward compatibility with the legacy Glue REST API.
Nested includes
Specify the full path through the relationship chain using dot notation:
GET /orders?include=items.concrete-products
This resolves items as a relationship of orders, then concrete-products as a relationship of items.
Flat includes
Flat includes list all desired relationships without specifying the nesting path:
GET /orders?include=items,concrete-products
If concrete-products is not a direct relationship of orders but is a relationship of items, the system automatically discovers the correct resolution path. This ensures backward compatibility with the legacy Glue REST API.
Auto-include
Relationships configured with autoInclude: true are automatically resolved without requiring a ?include= parameter:
includes:
- relationshipName: items
targetResource: OrderItems
autoInclude: true
uriVariableMappings:
orderReference: orderReference
When a resource with auto-include relationships is included in a response (either as a main resource or as an included resource), its auto-include relationships are transitively resolved.
Thank you!
For submitting the form