Skip to content

Commit

Permalink
feat: URI variables
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Oct 1, 2021
1 parent e649fa1 commit 2c8f0f0
Show file tree
Hide file tree
Showing 29 changed files with 806 additions and 416 deletions.
334 changes: 314 additions & 20 deletions docs/adr/0003-uri-variables.md
Expand Up @@ -14,7 +14,7 @@ URI variables are the URI template (e.g. `/books/{id}`) variables (e.g. `id`). W
<?php
use Company;

#[Get("/companies/{companyId}/users/{id}", [identifiers=["companyId" => [Company::class, "id"], "id" => [User::class, "id"]]])]
#[ApiResource("/companies/{companyId}/users/{id}", [identifiers=["companyId" => [Company::class, "id"], "id" => [User::class, "id"]]])]
class User {
#[ApiProperty(identifier=true)]
public $id;
Expand All @@ -31,40 +31,334 @@ To make this work, API Platform needs to know what property of the class User ha

## Decision Outcome

We will use a map to define URI variables, for now these options are available:
We will use a POPO to define URI variables, for now these options are available:

```
uriTemplate: [
'companyId' => [
'class' => Company::class,
'identifiers' => ['id'],
'composite_identifier' => true,
'property' => 'user'
uriVariables: [
'companyId' => new UriVariable(
targetClass: Company::class,
inverseProperty: null,
property: 'company'
identifiers: ['id'],
compositeIdentifier: true,
],
'id' => [
'class' => User::class,
'identifiers' => ['id']
]
'id' => new UriVariable(
targetClas: User::class,
identifiers: ['id']
)
]
```

Where `uriTemplate` keys are the URI template's variable names. Its value is a map where:
Where `uriVariables` keys are the URI template's variable names. Its value is a map where:

- `class` is the PHP FQDN to the class this value belongs to
- `identifiers` are the properties of the class to which we map the URI variable
- `composite_identifier` is used to match a single variable to multiple identifiers (`ida=1;idb=2` to `class::ida` and `class::idb`)
- `property` represents the property that has a link to the next identifier
- `targetClass` is the PHP FQDN of the class this value belongs to
- `property` represents the property, the URI Variable is mapped to in the current class
- `inverseProperty` represents the property, the URI Variable is mapped to in the related class and is not available in the current class
- `identifiers` are the properties of the targetClass to which we map the URI variable
- `compositeIdentifier` is used to match a single variable to multiple identifiers (`ida=1;idb=2` to `class::ida` and `class::idb`)

As of PHP 8.1, PHP will support [nested attributes](https://wiki.php.net/rfc/new_in_initializers). We'll introduce a proper class as an alternative to the associative array when PHP 8.1 will be released.
As of PHP 8.1, PHP will support [nested attributes](https://wiki.php.net/rfc/new_in_initializers), it'll be required to configure the `uriVariables`.

Thanks to these we can build our query which in this case is (pseudo-SQL):
Thanks to these we can build our query which in this case is (pseudo-SQL) to fetch the user belonging to a company (`/companies/{companyId}/users/{id}`):

```sql
SELECT * FROM User::class u
JOIN u.company c
WHERE u.id = :id AND c.id = :companyId
```

### Example for a User resource that belongs to a Company:

```php
<?php

#[ApiResource("/companies/{companyId}/users/{id}")]
class User {
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
<?php

#[ApiResource]
class Company {
public $id;
}
```

Generated DQL:

```sql
SELECT * FROM User::class u
JOIN Company::class c ON c.user = u.id AND c.id = :companyId
JOIN u.company c
WHERE u.id = :id AND c.id = :companyId
```

### Example the Company resource that belongs to a user (using the inverse relation):

```php
<?php

#[ApiResource]
class User {
public $id;
public Company $company;
}
```

```php
<?php

#[ApiResource("/users/{userId}/company", uriVariables: [
'userId' => new UriVariable(User::class, 'company')
])]
class Company {
public $id;
}
```

Note that the above is a shortcut for: `new UriVariable(targetClass: User::class, inverseProperty: 'company')`

Corresponding DQL:

```sql
SELECT * FROM Company::class c
JOIN User::class u WITH u.companyId = c.id
WHERE u.id = :id
```

Or if you have the inverse relation mapped to a property:


```php
<?php

#[ApiResource]
class User {
public $id;
public Company $company;
}
```

```php
<?php

#[ApiResource("/users/{userId}/company")]
class Company {
#[ApiProperty(identifier=true)]
public $id;

#[UriVariable('userId')]
/** @var User[] */
public $users;
}
```

Corresponding DQL:

```sql
SELECT * FROM Company::class c
JOIN c.users u
WHERE u.id = :id
```

### Example to get the users behind a company:

```php
<?php

#[ApiResource("/companies/{companyId}/users")]
#[GetCollection]
class User {
public $id;
#[UriVariable('companyId')]
public Company $company;
}
```

```php
<?php

#[ApiResource]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
```

Generated DQL:

```sql
SELECT * FROM User::class u
JOIN u.company c
WHERE c.id = :companyId
```

### Example for a User resource that belongs to a Company (complex definition):

```php
<?php

#[ApiResource("/companies/{companyId}/users/{id}", uriVariables: [
'companyId' => new UriVariable(
targetClass: Company::class,
property: 'company',
identifiers: ['id'],
compositeIdentifier: true
],
'id' => new UriVariable(
targetClass: User::class,
identifiers: ['id']
)
])]
class User {
#[ApiProperty(identifier=true)]
public $id;
public Company $company;
}
```

```php
<?php

#[ApiResource]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
```

Generated DQL:

```sql
SELECT * FROM User::class u
JOIN u.company c
WHERE u.id = :id AND c.id = :companyId
```

### Example the Company resource that belongs to a user (using the inverse relation, complex definition):

```php
<?php

#[ApiResource]
class User {
public $id;
public Company $company;
}
```

```php
<?php

#[ApiResource("/users/{userId}/company", uriVariables: [
'userId' => new UriVariable(
targetClass: User::class,
inverseProperty: 'company'
property: null,
identifiers: ['id'],
compositeIdentifier: true
)
])]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
```

Corresponding DQL:

```sql
SELECT * FROM Company::class c
JOIN User::class u WITH u.companyId = c.id
WHERE u.id = :id
```

Or if you have the inverse relation mapped to a property:


```php
<?php

#[ApiResource]
class User {
public $id;
public Company $company;
}
```

```php
<?php

#[ApiResource("/users/{userId}/company", uriVariables: [
'userId' => new UriVariable(
targetClass: User::class,
property: 'users',
identifiers: ['id'],

)
])]
class Company {
#[ApiProperty(identifier=true)]
public $id;

/** @var User[] */
public $users;
}
```

Corresponding DQL:

```sql
SELECT * FROM Company::class c
JOIN c.users u
WHERE u.id = :id
```

### Example to get the users behind a company (complex version):

```php
<?php

#[ApiResource("/companies/{companyId}/users", uriVariables: [
'companyId' => new UriVariable(
targetClass: Company::class,
identifiers: ['id'],
compositeIdentifier: true,
property: 'company'
)
])]
#[GetCollection]
class User {
#[ApiProperty(identifier=true)]
public $id;
public Company $company;
}
```

```php
<?php

#[ApiResource]
class Company {
#[ApiProperty(identifier=true)]
public $id;
}
```

Generated DQL:

```sql
SELECT * FROM User::class u
JOIN u.company c
WHERE c.id = :companyId
```

## Links

* Supersedes the [0001-resource-identifiers](0001-resource-identifiers.md) ADR.
Expand Down
10 changes: 5 additions & 5 deletions src/Api/IdentifiersExtractor.php
Expand Up @@ -57,17 +57,17 @@ public function getIdentifiersFromItem($item, string $operationName = null, arra
$operation = $context['operation'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation($operationName);

foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariableDefinition) {
if (1 < \count($uriVariableDefinition['identifiers'])) {
if (1 < \count($uriVariableDefinition->getIdentifiers())) {
$compositeIdentifiers = [];
foreach ($uriVariableDefinition['identifiers'] as $identifier) {
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $uriVariableDefinition['class'], $identifier, $parameterName);
foreach ($uriVariableDefinition->getIdentifiers() as $identifier) {
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $uriVariableDefinition->getTargetClass() ?? $resourceClass, $identifier, $parameterName);
}

$identifiers[($operation->getExtraProperties()['is_legacy_resource_metadata'] ?? false) ? 'id' : $parameterName] = CompositeIdentifierParser::stringify($compositeIdentifiers);
continue;
}

$identifiers[$parameterName] = $this->getIdentifierValue($item, $uriVariableDefinition['class'], $uriVariableDefinition['identifiers'][0], $parameterName);
$identifiers[$parameterName] = $this->getIdentifierValue($item, $uriVariableDefinition->getTargetClass(), $uriVariableDefinition->getIdentifiers()[0], $parameterName);
}

return $identifiers;
Expand Down Expand Up @@ -126,7 +126,7 @@ private function resolveIdentifierValue($identifierValue, string $parameterName)
$relatedOperation = $this->resourceMetadataFactory->create($relatedResourceClass)->getOperation();
$relatedIdentifiers = $relatedOperation->getUriVariables();
if (1 === \count($relatedIdentifiers)) {
$identifierValue = $this->getIdentifierValue($identifierValue, $relatedResourceClass, current($relatedIdentifiers)['identifiers'][0], $parameterName);
$identifierValue = $this->getIdentifierValue($identifierValue, $relatedResourceClass, current($relatedIdentifiers)->getIdentifiers()[0], $parameterName);

if ($identifierValue instanceof \Stringable || is_scalar($identifierValue) || method_exists($identifierValue, '__toString')) {
return (string) $identifierValue;
Expand Down

0 comments on commit 2c8f0f0

Please sign in to comment.