Skip to content

Commit

Permalink
Merge pull request #1206 from soyuka/improve-subresources
Browse files Browse the repository at this point in the history
Implement subresource metadata and annotation
  • Loading branch information
dunglas committed Jul 3, 2017
2 parents 7b8f416 + 8a393ca commit eb47c57
Show file tree
Hide file tree
Showing 24 changed files with 347 additions and 46 deletions.
5 changes: 0 additions & 5 deletions src/Annotation/ApiProperty.php
Expand Up @@ -67,9 +67,4 @@ final class ApiProperty
* @var array
*/
public $attributes = [];

/**
* @var bool
*/
public $subresource;
}
26 changes: 26 additions & 0 deletions src/Annotation/ApiSubresource.php
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Annotation;

/**
* Property annotation.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*
* @Annotation
* @Target({"METHOD", "PROPERTY"})
*/
final class ApiSubresource
{
}
Expand Up @@ -28,6 +28,11 @@
<argument type="service" id="annotation_reader" />
<argument type="service" id="api_platform.metadata.property.metadata_factory.annotation.inner" />
</service>

<service id="api_platform.metadata.subresource.metadata_factory.annotation" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="30" class="ApiPlatform\Core\Metadata\Property\Factory\AnnotationSubresourceMetadataFactory" public="false">
<argument type="service" id="annotation_reader" />
<argument type="service" id="api_platform.metadata.subresource.metadata_factory.annotation.inner" />
</service>
</services>

</container>
18 changes: 8 additions & 10 deletions src/Bridge/Symfony/Routing/ApiLoader.php
Expand Up @@ -141,21 +141,19 @@ private function computeSubresourceOperations(RouteCollection $routeCollection,
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);

if (null === $propertyMetadata->hasSubresource()) {
if (!$propertyMetadata->hasSubresource()) {
continue;
}

$isCollection = $propertyMetadata->getType()->isCollection();
$subresource = $isCollection ? $propertyMetadata->getType()->getCollectionValueType()->getClassName() : $propertyMetadata->getType()->getClassName();

$propertyName = $this->routeNameResolver($property, $isCollection);
$subresource = $propertyMetadata->getSubresource();
$propertyName = $this->routeNameResolver($property, $subresource->isCollection());

$operation = [
'property' => $property,
'collection' => $isCollection,
'collection' => $subresource->isCollection(),
];

$visiting = "$rootResourceClass $resourceClass $propertyName $subresource";
$visiting = "$rootResourceClass $resourceClass $propertyName {$subresource->getResourceClass()}";

if (in_array($visiting, $visited, true)) {
continue;
Expand Down Expand Up @@ -183,12 +181,12 @@ private function computeSubresourceOperations(RouteCollection $routeCollection,
[
'_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
'_format' => null,
'_api_resource_class' => $subresource,
'_api_resource_class' => $subresource->getResourceClass(),
'_api_subresource_operation_name' => $operation['route_name'],
'_api_subresource_context' => [
'property' => $operation['property'],
'identifiers' => $operation['identifiers'],
'collection' => $isCollection,
'collection' => $subresource->isCollection(),
],
],
[],
Expand All @@ -200,7 +198,7 @@ private function computeSubresourceOperations(RouteCollection $routeCollection,

$routeCollection->add($operation['route_name'], $route);

$this->computeSubresourceOperations($routeCollection, $subresource, $rootResourceClass, $operation, $visited);
$this->computeSubresourceOperations($routeCollection, $subresource->getResourceClass(), $rootResourceClass, $operation, $visited);
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/Metadata/Extractor/XmlExtractor.php
Expand Up @@ -130,7 +130,10 @@ private function getProperties(\SimpleXMLElement $resource): array
'identifier' => $this->phpize($property, 'identifier', 'bool'),
'iri' => $this->phpize($property, 'iri', 'string'),
'attributes' => $this->getAttributes($property, 'attribute'),
'subresource' => $this->phpize($property, 'subresource', 'bool'),
'subresource' => $property->subresource ? [
'collection' => $this->phpize($property->subresource, 'collection', 'bool'),
'resourceClass' => $this->phpize($property->subresource, 'resourceClass', 'string'),
] : null,
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/Metadata/Extractor/YamlExtractor.php
Expand Up @@ -107,7 +107,7 @@ private function extractProperties(array $resourceYaml, string $resourceName, st
'identifier' => $this->phpize($propertyValues, 'identifier', 'bool'),
'iri' => $this->phpize($propertyValues, 'iri', 'string'),
'attributes' => $propertyValues['attributes'] ?? null,
'subresource' => $this->phpize($propertyValues, 'subresource', 'bool'),
'subresource' => $propertyValues['subresource'] ?? null,
];
}
}
Expand Down
Expand Up @@ -117,13 +117,12 @@ private function createMetadata(ApiProperty $annotation, PropertyMetadata $paren
$annotation->identifier,
$annotation->iri,
null,
$annotation->attributes,
$annotation->subresource
$annotation->attributes
);
}

$propertyMetadata = $parentPropertyMetadata;
foreach ([['get', 'description'], ['is', 'readable'], ['is', 'writable'], ['is', 'readableLink'], ['is', 'writableLink'], ['is', 'required'], ['get', 'iri'], ['is', 'identifier'], ['get', 'attributes'], ['has', 'subresource']] as $property) {
foreach ([['get', 'description'], ['is', 'readable'], ['is', 'writable'], ['is', 'readableLink'], ['is', 'writableLink'], ['is', 'required'], ['get', 'iri'], ['is', 'identifier'], ['get', 'attributes']] as $property) {
if (null !== $value = $annotation->{$property[1]}) {
$propertyMetadata = $this->createWith($propertyMetadata, $property, $value);
}
Expand Down
@@ -0,0 +1,88 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Metadata\Property\Factory;

use ApiPlatform\Core\Annotation\ApiSubresource;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Property\SubresourceMetadata;
use ApiPlatform\Core\Util\Reflection;
use Doctrine\Common\Annotations\Reader;

/**
* Adds subresources to the properties metadata from {@see ApiResource} annotations.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
final class AnnotationSubresourceMetadataFactory implements PropertyMetadataFactoryInterface
{
private $reader;
private $decorated;

public function __construct(Reader $reader, PropertyMetadataFactoryInterface $decorated)
{
$this->reader = $reader;
$this->decorated = $decorated;
}

/**
* {@inheritdoc}
*/
public function create(string $resourceClass, string $property, array $options = []): PropertyMetadata
{
$propertyMetadata = $this->decorated->create($resourceClass, $property, $options);

try {
$reflectionClass = new \ReflectionClass($resourceClass);
} catch (\ReflectionException $reflectionException) {
return $propertyMetadata;
}

if ($reflectionClass->hasProperty($property)) {
$annotation = $this->reader->getPropertyAnnotation($reflectionClass->getProperty($property), ApiSubresource::class);

if (null !== $annotation) {
return $this->updateMetadata($annotation, $propertyMetadata);
}
}

foreach (array_merge(Reflection::ACCESSOR_PREFIXES, Reflection::MUTATOR_PREFIXES) as $prefix) {
$methodName = $prefix.ucfirst($property);
if (!$reflectionClass->hasMethod($methodName)) {
continue;
}

$reflectionMethod = $reflectionClass->getMethod($methodName);
if (!$reflectionMethod->isPublic()) {
continue;
}

$annotation = $this->reader->getMethodAnnotation($reflectionMethod, ApiSubresource::class);

if (null !== $annotation) {
return $this->updateMetadata($annotation, $propertyMetadata);
}
}

return $propertyMetadata;
}

private function updateMetadata(ApiSubresource $annotation, PropertyMetadata $propertyMetadata): PropertyMetadata
{
$type = $propertyMetadata->getType();
$isCollection = $type->isCollection();
$resourceClass = $isCollection ? $type->getCollectionValueType()->getClassName() : $type->getClassName();

return $propertyMetadata->withSubresource(new SubresourceMetadata($resourceClass, $isCollection));
}
}
40 changes: 34 additions & 6 deletions src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\Core\Exception\PropertyNotFoundException;
use ApiPlatform\Core\Metadata\Extractor\ExtractorInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Property\SubresourceMetadata;

/**
* Creates properties's metadata using an extractor.
Expand Down Expand Up @@ -58,7 +59,7 @@ public function create(string $resourceClass, string $property, array $options =
return $this->update($parentPropertyMetadata, $propertyMetadata);
}

return new PropertyMetadata(
return ($metadata = new PropertyMetadata(
null,
$propertyMetadata['description'],
$propertyMetadata['readable'],
Expand All @@ -69,9 +70,8 @@ public function create(string $resourceClass, string $property, array $options =
$propertyMetadata['identifier'],
$propertyMetadata['iri'],
null,
$propertyMetadata['attributes'],
$propertyMetadata['subresource']
);
$propertyMetadata['attributes']
))->withSubresource($this->createSubresourceMetadata($propertyMetadata['subresource'], $metadata));
}

/**
Expand Down Expand Up @@ -114,7 +114,6 @@ private function update(PropertyMetadata $propertyMetadata, array $metadata): Pr
'identifier' => 'is',
'iri' => 'get',
'attributes' => 'get',
'subresource' => 'has',
];

foreach ($metadataAccessors as $metadataKey => $accessorPrefix) {
Expand All @@ -125,6 +124,35 @@ private function update(PropertyMetadata $propertyMetadata, array $metadata): Pr
$propertyMetadata = $propertyMetadata->{'with'.ucfirst($metadataKey)}($metadata[$metadataKey]);
}

return $propertyMetadata;
return $propertyMetadata->withSubresource($this->createSubresourceMetadata($metadata['subresource'], $propertyMetadata));
}

/**
* Creates a SubresourceMetadata.
*
* @param bool|null|array $subresource the subresource metadata coming from XML or YAML
* @param PropertyMetadata $propertyMetadata the current property metadata
*
* @return SubresourceMetadata|null
*/
private function createSubresourceMetadata($subresource, PropertyMetadata $propertyMetadata)
{
if (!$subresource) {
return null;
}

$type = $propertyMetadata->getType();

if (null !== $type) {
$isCollection = $type->isCollection();
$resourceClass = $isCollection ? $type->getCollectionValueType()->getClassName() : $type->getClassName();
} elseif (isset($subresource['resourceClass'])) {
$resourceClass = $subresource['resourceClass'];
$isCollection = $subresource['collection'] ?? true;
} else {
return null;
}

return new SubresourceMetadata($resourceClass, $isCollection);
}
}
28 changes: 25 additions & 3 deletions src/Metadata/Property/PropertyMetadata.php
Expand Up @@ -35,7 +35,7 @@ final class PropertyMetadata
private $attributes;
private $subresource;

public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, bool $subresource = null)
public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null)
{
$this->type = $type;
$this->description = $description;
Expand Down Expand Up @@ -347,12 +347,34 @@ public function withChildInherited(string $childInherited): self
return $metadata;
}

public function hasSubresource()
/**
* Represents whether the property has a subresource.
*
* @return bool
*/
public function hasSubresource(): bool
{
return $this->subresource !== null;
}

/**
* Gets the subresource metadata.
*
* @return SubresourceMetadata|null
*/
public function getSubresource()
{
return $this->subresource;
}

public function withSubresource(bool $subresource = null): self
/**
* Returns a new instance with the given subresource.
*
* @param SubresourceMetadata $subresource
*
* @return self
*/
public function withSubresource(SubresourceMetadata $subresource = null): self
{
$metadata = clone $this;
$metadata->subresource = $subresource;
Expand Down

0 comments on commit eb47c57

Please sign in to comment.