- Status: accepted
- Deciders: @dunglas, @soyuka
Implementation: #4408
For reference see the previously implemented Resource identifier ADR.
URI variables are the URI template (e.g. /books/{id}
) variables (e.g. id
). When defining alternate resources (see Resource definition), we need more information than only a property and a class. The tuple accepted in Resource identifier is not enough. For example:
<?php
use Company;
#[ApiResource("/companies/{companyId}/employees/{id}", [identifiers=["companyId" => [Company::class, "id"], "id" => [Employee::class, "id"]]])]
class Employee {
#[ApiProperty(identifier=true)]
public $id;
public Company $company;
}
To make this work, API Platform needs to know what property of the class Employee has the link to Company.
- Keeping the tuple but adding a third value which is the property referenced by the parent class.
- Using a map to allow granularity configuration of the URI variables.
We will use a POPO to define URI variables, for now these options are available:
uriVariables: [
'companyId' => new UriVariable(
targetClass: Company::class,
targetProperty: null,
property: 'company'
identifiers: ['id'],
compositeIdentifier: true,
],
'id' => new UriVariable(
targetClas: Employee::class,
identifiers: ['id']
)
]
Where uriVariables
keys are the URI template's variable names. Its value is a map where:
targetClass
is the PHP FQDN of the class this value belongs toproperty
represents the property, the URI Variable is mapped to in the current classtargetProperty
represents the property, the URI Variable is mapped to in the related class and is not available in the current classidentifiers
are the properties of the targetClass to which we map the URI variablecompositeIdentifier
is used to match a single variable to multiple identifiers (ida=1;idb=2
toclass::ida
andclass::idb
)
As of PHP 8.1, PHP will support nested attributes, it'll be required to configure the uriVariables
.
Thanks to these we can build our query which in this case is (pseudo-SQL) to fetch the employee belonging to a company (/companies/{companyId}/employees/{id}
):
SELECT * FROM Employee::class e
JOIN e.company c
WHERE e.id = :id AND c.id = :companyId
<?php
#[ApiResource("/companies/{companyId}/employees/{id}")]
class Employee {
public $id;
// Note that this is the same as defining UriVariable(property: company), see below on the complex example
#[UriVariable('companyId')]
public Company $company;
}
<?php
#[ApiResource]
class Company {
public $id;
}
Generated DQL:
SELECT * FROM Employee::class e
JOIN e.company c
WHERE e.id = :id AND c.id = :companyId
<?php
#[ApiResource]
class Employee {
public $id;
public Company $company;
}
<?php
#[ApiResource("/employee/{employeeId}/company", uriVariables: [
'employeeId' => new UriVariable(Employee::class, 'company')
])]
class Company {
public $id;
}
Note that the above is a shortcut for: new UriVariable(targetClass: Employee::class, targetProperty: 'company')
Corresponding DQL:
SELECT * FROM Company::class c
JOIN Employee::class e WITH e.companyId = c.id
WHERE e.id = :id
Or if you have the inverse relation mapped to a property:
<?php
#[ApiResource]
class Employee {
public $id;
public Company $company;
}
<?php
#[ApiResource("/employees/{employeeId}/company")]
class Company {
#[ApiProperty(identifier=true)]
public $id;
#[UriVariable('employeeId')]
/** @var Employee[] */
public $employees;
}
Corresponding DQL:
SELECT * FROM Company::class c
JOIN c.employees u
WHERE u.id = :id
<?php
#[ApiResource("/companies/{companyId}/employees")]
#[GetCollection]
class Employee {
public $id;
#[UriVariable('companyId')]
public Company $company;
}
<?php
#[ApiResource]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
Generated DQL:
SELECT * FROM Employee::class e
JOIN e.company c
WHERE c.id = :companyId
<?php
#[ApiResource("/companies/{companyId}/employees/{id}", uriVariables: [
'companyId' => new UriVariable(
targetClass: Company::class,
property: 'company',
identifiers: ['id'],
compositeIdentifier: true
],
'id' => new UriVariable(
targetClass: Employee::class,
identifiers: ['id']
)
])]
class Employee {
#[ApiProperty(identifier=true)]
public $id;
public Company $company;
}
<?php
#[ApiResource]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
Generated DQL:
SELECT * FROM Employee::class e
JOIN e.company c
WHERE e.id = :id AND c.id = :companyId
Example the Company resource that belongs to a Employee (using the inverse relation, complex definition):
<?php
#[ApiResource]
class Employee {
public $id;
public Company $company;
}
<?php
#[ApiResource("/employees/{employeeId}/company", uriVariables: [
'employeeId' => new UriVariable(
targetClass: Employee::class,
targetProperty: 'company'
property: null,
identifiers: ['id'],
compositeIdentifier: true
)
])]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
Corresponding DQL:
SELECT * FROM Company::class c
JOIN Employee::class e WITH e.companyId = c.id
WHERE e.id = :id
Or if you have the inverse relation mapped to a property:
<?php
#[ApiResource]
class Employee {
public $id;
public Company $company;
}
<?php
#[ApiResource("/employees/{employeeId}/company", uriVariables: [
'employeeId' => new UriVariable(
targetClass: Employee::class,
property: 'employees',
identifiers: ['id'],
)
])]
class Company {
#[ApiProperty(identifier=true)]
public $id;
/** @var Employee[] */
public $employees;
}
Corresponding DQL:
SELECT * FROM Company::class c
JOIN c.employees e
WHERE e.id = :id
<?php
#[ApiResource("/companies/{companyId}/employees", uriVariables: [
'companyId' => new UriVariable(
targetClass: Company::class,
identifiers: ['id'],
compositeIdentifier: true,
property: 'company'
)
])]
#[GetCollection]
class Employee {
#[ApiProperty(identifier=true)]
public $id;
public Company $company;
}
<?php
#[ApiResource]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
Generated DQL:
SELECT * FROM Employee::class e
JOIN e.company c
WHERE c.id = :companyId
- Supersedes the 0001-resource-identifiers ADR.