Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
abluchet authored and soyuka committed Feb 6, 2017
1 parent b76438e commit e721183
Show file tree
Hide file tree
Showing 48 changed files with 776 additions and 87 deletions.
150 changes: 150 additions & 0 deletions features/main/embedded.feature
@@ -0,0 +1,150 @@
Feature: Embedded resources support
In order to use a hypermedia API
As a client software developer
I need to be able to retrieve embedded resources only

@dropSchema
Scenario: Just delete previous stuff pls

@createSchema
Scenario: Create a third level
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/third_levels" with body:
"""
{"level": 3}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/ThirdLevel",
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"id": 1,
"level": 3,
"test": true
}
"""

Scenario: Create a related dummy
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/related_dummies" with body:
"""
{"thirdLevel": "/third_levels/1"}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/RelatedDummy",
"@id": "/related_dummies/1",
"@type": "https://schema.org/Product",
"name": null,
"dummyDate": null,
"thirdLevel": "/third_levels/1",
"relatedToDummyFriend": null,
"dummyBoolean": null,
"id": 1,
"symfony": "symfony",
"age": null
}
"""

Scenario: Create a dummy with relations
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummies" with body:
"""
{
"name": "Dummy with relations",
"relatedDummy": "http://example.com/related_dummies/1",
"relatedDummies": [
"/related_dummies/1"
],
"name_converted": null
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/Dummy",
"@id": "/dummies/1",
"@type": "Dummy",
"description": null,
"dummy": null,
"dummyBoolean": null,
"dummyDate": null,
"dummyFloat": null,
"dummyPrice": null,
"relatedDummy": "/related_dummies/1",
"relatedDummies": [
"/related_dummies/1"
],
"jsonData": [],
"name_converted": null,
"id": 1,
"name": "Dummy with relations",
"alias": null
}
"""

Scenario: Get the embedded relation collection
When I send a "GET" request to "/dummies/1/related_dummies"
And the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/RelatedDummy",
"@id": "/related_dummies",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/related_dummies/1",
"@type": "https://schema.org/Product",
"name": null,
"dummyDate": null,
"thirdLevel": "/third_levels/1",
"relatedToDummyFriend": [],
"dummyBoolean": null,
"id": 1,
"symfony": "symfony",
"age": null
}
],
"hydra:totalItems": 1
}
"""

@dropSchema
Scenario: Get the embedded relation collection
When I send a "GET" request to "/dummies/1/related_dummies/1/third_levels"
And the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/ThirdLevel",
"@id": "/third_levels",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"id": 1,
"level": 3,
"test": true
}
],
"hydra:totalItems": 1
}
"""

5 changes: 5 additions & 0 deletions src/Annotation/ApiProperty.php
Expand Up @@ -65,4 +65,9 @@ final class ApiProperty
* @var array
*/
public $attributes = [];

/**
* @var string
*/
public $subcollection;
}
33 changes: 33 additions & 0 deletions src/Api/ResourceClassResolver.php
Expand Up @@ -60,6 +60,39 @@ public function getResourceClass($value, string $resourceClass = null, bool $str
return $resourceClass;
}

/**
* {@inheritdoc}
*/
public function getResourceClassFromContext($value, array $context = null, bool $strict = false): string
{
$resourceClass = $context['resource_class'] ?? null;

if (isset($context['subcollection_resource_class']) && null !== $context['subcollection_resource_class']) {
$resourceClass = $context['subcollection_resource_class'];
}

if (is_object($value) && !$value instanceof PaginatorInterface) {
$typeToFind = $type = $this->getObjectClass($value);
if (null === $resourceClass) {
$resourceClass = $typeToFind;
}
} elseif (null === $resourceClass) {
throw new InvalidArgumentException(sprintf('No resource class found.'));
} else {
$typeToFind = $type = $resourceClass;
}

if (($strict && isset($type) && $resourceClass !== $type) || !$this->isResourceClass($typeToFind)) {
if (is_subclass_of($type, $resourceClass) && $this->isResourceClass($resourceClass)) {
return $type;
}

throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s"', $typeToFind));
}

return $resourceClass;
}

/**
* {@inheritdoc}
*/
Expand Down
13 changes: 13 additions & 0 deletions src/Api/ResourceClassResolverInterface.php
Expand Up @@ -33,6 +33,19 @@ interface ResourceClassResolverInterface
*/
public function getResourceClass($value, string $resourceClass = null, bool $strict = false): string;

/**
* Guesses the associated resource.
*
* @param mixed $value
* @param array|null $context
* @param bool $strict
*
* @throws InvalidArgumentException
*
* @return string
*/
public function getResourceClassFromContext($value, array $context = null, bool $strict = false): string;

/**
* Is the given class a resource class?
*
Expand Down
128 changes: 128 additions & 0 deletions src/Bridge/Doctrine/Orm/SubcollectionDataProvider.php
@@ -0,0 +1,128 @@
<?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.
*/

namespace ApiPlatform\Core\Bridge\Doctrine\Orm;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\IdentifierManagerTrait;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Core\DataProvider\SubcollectionDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\Mapping\ClassMetadataInfo;

/**
* Subcollection data provider for the Doctrine ORM.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
class SubcollectionDataProvider implements SubcollectionDataProviderInterface
{
use IdentifierManagerTrait;

private $managerRegistry;
private $collectionExtensions;

/**
* @param ManagerRegistry $managerRegistry
* @param PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory
* @param PropertyMetadataFactoryInterface $propertyMetadataFactory
* @param QueryItemExtensionInterface[] $itemExtensions
*/
public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, array $collectionExtensions = [])
{
$this->managerRegistry = $managerRegistry;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->collectionExtensions = $collectionExtensions;
}

/**
* {@inheritdoc}
*
* @throws RuntimeException
*/
public function getSubcollection(string $resourceClass, array $identifiers, array $context, string $operationName)
{
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
if (null === $manager) {
throw new ResourceClassNotSupportedException();
}

$repository = $manager->getRepository($resourceClass);
if (!method_exists($repository, 'createQueryBuilder')) {
throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');
}

$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();
$previousQueryBuilder = null;
$previousAlias = null;

$num = count($context['identifiers']);

while ($num--) {
list($identifier, $identifierResourceClass) = $context['identifiers'][$num];
$previousIdentifier = $context['identifiers'][$num + 1][0] ?? $context['property'];

$classMetadata = $manager->getClassMetadata($identifierResourceClass);

$qb = $manager->createQueryBuilder();
$alias = $queryNameGenerator->generateJoinAlias($identifier);

if (null !== $previousIdentifier) {
//MANY_TO_MANY relations needs an explicit join so that the identifier part can be retrieved
if ($classMetadata->getAssociationMapping($previousIdentifier)['type'] === ClassMetadataInfo::MANY_TO_MANY) {
$joinAlias = $queryNameGenerator->generateJoinAlias($previousIdentifier);

$qb->select($joinAlias)
->from($identifierResourceClass, $alias)
->innerJoin("$alias.$previousIdentifier", $joinAlias);
} else {
$qb->select("IDENTITY($alias.$previousIdentifier)")
->from($identifierResourceClass, $alias);
}
}

$normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass);

foreach ($normalizedIdentifiers as $key => $value) {
$placeholder = $queryNameGenerator->generateParameterName($key);
$qb->andWhere("$alias.$key = :$placeholder");
$queryBuilder->setParameter($placeholder, $value);
}

if (null === $previousQueryBuilder) {
$previousQueryBuilder = $qb;
} else {
$previousQueryBuilder->andWhere($qb->expr()->in($previousAlias, $qb->getDQL()));
}

$previousAlias = $alias;
}

$queryBuilder->where(
$queryBuilder->expr()->in('o', $previousQueryBuilder->getDQL())
);

foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName);

if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {
return $extension->getResult($queryBuilder);
}
}

return $queryBuilder->getQuery()->getResult();
}
}
Expand Up @@ -11,6 +11,7 @@

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler;

use ApiPlatform\Core\Util\OperationTypes;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -29,8 +30,9 @@ final class DataProviderPass implements CompilerPassInterface
*/
public function process(ContainerBuilder $container)
{
$this->registerDataProviders($container, 'collection');
$this->registerDataProviders($container, 'item');
foreach (OperationTypes::TYPES as $type) {
$this->registerDataProviders($container, $type);
}
}

/**
Expand Down
Expand Up @@ -37,9 +37,13 @@ public function process(ContainerBuilder $container)

$collectionDataProviderDefinition = $container->getDefinition('api_platform.doctrine.orm.collection_data_provider');
$itemDataProviderDefinition = $container->getDefinition('api_platform.doctrine.orm.item_data_provider');
$subcollectionDataProviderDefinition = $container->getDefinition('api_platform.doctrine.orm.subcollection_data_provider');

$collectionDataProviderDefinition->replaceArgument(1, $this->findSortedServices($container, 'api_platform.doctrine.orm.query_extension.collection'));
$collectionExtensions = $this->findSortedServices($container, 'api_platform.doctrine.orm.query_extension.collection');

$collectionDataProviderDefinition->replaceArgument(1, $collectionExtensions);
$itemDataProviderDefinition->replaceArgument(3, $this->findSortedServices($container, 'api_platform.doctrine.orm.query_extension.item'));
$subcollectionDataProviderDefinition->replaceArgument(3, $collectionExtensions);
}

/**
Expand Down

0 comments on commit e721183

Please sign in to comment.