Skip to content

Commit

Permalink
Merge 4c9185b into 9c348d2
Browse files Browse the repository at this point in the history
  • Loading branch information
norkunas committed Mar 25, 2021
2 parents 9c348d2 + 4c9185b commit 5844f64
Show file tree
Hide file tree
Showing 17 changed files with 354 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@

## 2.6.4

* JSON Schema: Fix generating property schema with Unique restriction (#4159)
* Serializer: Fix denormalization of basic property-types in XML and CSV (#3191)
* Serializer: Fix denormalization of collection with one element in XML (#4154)
* JSON Schema: Manage Sequentially and AtLeastOneOf constraints when generating property metadata (#4139 and #4147)
Expand Down
20 changes: 18 additions & 2 deletions features/doctrine/eager_loading.feature
Expand Up @@ -8,7 +8,7 @@ Feature: Eager Loading
Scenario: Eager loading for a relation
Given there is a RelatedDummy with 2 friends
When I send a "GET" request to "/related_dummies/1"
And the response status code should be 200
Then the response status code should be 200
And the DQL should be equal to:
"""
SELECT o, thirdLevel_a1, relatedToDummyFriend_a2, dummyFriend_a3
Expand Down Expand Up @@ -42,7 +42,7 @@ Feature: Eager Loading
Scenario: Eager loading for a relation and a search filter
Given there is a RelatedDummy with 2 friends
When I send a "GET" request to "/related_dummies?relatedToDummyFriend.dummyFriend=2"
And the response status code should be 200
Then the response status code should be 200
And the DQL should be equal to:
"""
SELECT o, thirdLevel_a4, relatedToDummyFriend_a1, dummyFriend_a5
Expand All @@ -59,6 +59,22 @@ Feature: Eager Loading
ORDER BY o.id ASC
"""

Scenario: Eager loading for a relation and a property filter with multiple relations
Given there is a dummy travel
When I send a "GET" request to "/dummy_travels/1?properties[]=confirmed&properties[car][]=brand&properties[passenger][]=nickname"
Then the response status code should be 200
And the JSON node "confirmed" should be equal to "true"
And the JSON node "car.carBrand" should be equal to "DummyBrand"
And the JSON node "passenger.nickname" should be equal to "Tom"
And the DQL should be equal to:
"""
SELECT o, car_a1, passenger_a2
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTravel o
LEFT JOIN o.car car_a1
LEFT JOIN o.passenger passenger_a2
WHERE o.id = :id_id
"""

Scenario: Eager loading for a relation with complex sub-query filter
Given there is a RelatedDummy with 2 friends
When I send a "GET" request to "/related_dummies?complex_sub_query_filter=1"
Expand Down
4 changes: 2 additions & 2 deletions features/filter/property_filter.feature
Expand Up @@ -7,14 +7,14 @@ Feature: Set properties to include
Scenario: Test properties filter
Given there are 1 dummy objects with relatedDummy and its thirdLevel
When I send a "GET" request to "/dummies/1?properties[]=name&properties[]=alias&properties[]=relatedDummy&properties[]=name_converted"
And the JSON node "name" should be equal to "Dummy #1"
Then the JSON node "name" should be equal to "Dummy #1"
And the JSON node "alias" should be equal to "Alias #0"
And the JSON node "relatedDummies" should not exist
And the JSON node "name_converted" should exist

Scenario: Test relation embedding
When I send a "GET" request to "/dummies/1?properties[]=name&properties[]=alias&properties[relatedDummy][]=name"
And the JSON node "name" should be equal to "Dummy #1"
Then the JSON node "name" should be equal to "Dummy #1"
And the JSON node "alias" should be equal to "Alias #0"
And the JSON node "relatedDummy.name" should be equal to "RelatedDummy #1"
And the JSON node "relatedDummies" should not exist
7 changes: 4 additions & 3 deletions src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php
Expand Up @@ -167,10 +167,11 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
continue;
}

// prepare the child context
$childNormalizationContext = $normalizationContext;
if (isset($normalizationContext[AbstractNormalizer::ATTRIBUTES])) {
if ($inAttributes = isset($normalizationContext[AbstractNormalizer::ATTRIBUTES][$association])) {
// prepare the child context
$normalizationContext[AbstractNormalizer::ATTRIBUTES] = $normalizationContext[AbstractNormalizer::ATTRIBUTES][$association];
$childNormalizationContext[AbstractNormalizer::ATTRIBUTES] = $normalizationContext[AbstractNormalizer::ATTRIBUTES][$association];
}
} else {
$inAttributes = null;
Expand Down Expand Up @@ -236,7 +237,7 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
}
}

$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $normalizationContext, $isLeftJoin, $joinCount, $currentDepth);
$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $childNormalizationContext, $isLeftJoin, $joinCount, $currentDepth);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/validator.xml
Expand Up @@ -34,6 +34,10 @@
<tag name="api_platform.metadata.property_schema_restriction"/>
</service>

<service id="api_platform.metadata.property_schema.unique_restriction" class="ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaUniqueRestriction" public="false">
<tag name="api_platform.metadata.property_schema_restriction"/>
</service>

<service id="api_platform.listener.view.validate" class="ApiPlatform\Core\Validator\EventListener\ValidateListener">
<argument type="service" id="api_platform.validator" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
Expand Down
@@ -0,0 +1,40 @@
<?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\Bridge\Symfony\Validator\Metadata\Property\Restriction;

use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Unique;

/**
* @author Tomas Norkūnas <norkunas.tom@gmail.com>
*/
final class PropertySchemaUniqueRestriction implements PropertySchemaRestrictionMetadataInterface
{
/**
* {@inheritdoc}
*/
public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array
{
return ['uniqueItems' => true];
}

/**
* {@inheritdoc}
*/
public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool
{
return $constraint instanceof Unique;
}
}
44 changes: 44 additions & 0 deletions tests/Behat/DoctrineContext.php
Expand Up @@ -49,9 +49,11 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyImmutableDate as DummyImmutableDateDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyMercure as DummyMercureDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyOffer as DummyOfferDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyPassenger as DummyPassengerDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyProduct as DummyProductDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyProperty as DummyPropertyDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyTableInheritanceNotApiResourceChild as DummyTableInheritanceNotApiResourceChildDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyTravel as DummyTravelDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\EmbeddableDummy as EmbeddableDummyDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\EmbeddedDummy as EmbeddedDummyDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\FileConfigDummy as FileConfigDummyDocument;
Expand Down Expand Up @@ -118,9 +120,11 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyImmutableDate;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyMercure;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyOffer;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyPassenger;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProduct;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProperty;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceNotApiResourceChild;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTravel;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ExternalUser;
Expand Down Expand Up @@ -1141,6 +1145,30 @@ public function thereIsAFooEntityWithRelatedBars()
$this->manager->flush();
}

/**
* @Given there is a dummy travel
*/
public function thereIsADummyTravel()
{
$car = $this->buildDummyCar();
$car->setName('model x');
$car->setCanSell(true);
$car->setAvailableAt(new \DateTime());
$this->manager->persist($car);

$passenger = $this->buildDummyPassenger();
$passenger->nickname = 'Tom';
$this->manager->persist($passenger);

$travel = $this->buildDummyTravel();
$travel->car = $car;
$travel->passenger = $passenger;
$travel->confirmed = true;
$this->manager->persist($travel);

$this->manager->flush();
}

/**
* @Given there is a RelatedDummy with :nb friends
*/
Expand Down Expand Up @@ -1878,6 +1906,22 @@ private function buildDummyCarColor()
return $this->isOrm() ? new DummyCarColor() : new DummyCarColorDocument();
}

/**
* @return DummyPassenger|DummyPassengerDocument
*/
private function buildDummyPassenger()
{
return $this->isOrm() ? new DummyPassenger() : new DummyPassengerDocument();
}

/**
* @return DummyTravel|DummyTravelDocument
*/
private function buildDummyTravel()
{
return $this->isOrm() ? new DummyTravel() : new DummyTravelDocument();
}

/**
* @return DummyDate|DummyDateDocument
*/
Expand Down
Expand Up @@ -693,6 +693,7 @@ public function testAttributes()
$relationPropertyMetadata = new PropertyMetadata();
$relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(false);

$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', ['serializer_groups' => ['foo']])->willReturn($relationPropertyMetadata)->shouldBeCalled();
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo']])->willReturn($relationPropertyMetadata)->shouldBeCalled();

$idPropertyMetadata = new PropertyMetadata();
Expand All @@ -707,6 +708,7 @@ public function testAttributes()

$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
$classMetadataProphecy->associationMappings = [
'relatedDummies' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class],
'relatedDummy' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class],
];

Expand All @@ -727,8 +729,10 @@ public function testAttributes()
$queryBuilderProphecy->getRootAliases()->willReturn(['o']);
$queryBuilderProphecy->getEntityManager()->willReturn($emProphecy);

$queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->leftJoin('o.relatedDummies', 'relatedDummies_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a2')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummies_a1.{id,name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy_a2.{id,name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$request = Request::create('/api/dummies', 'GET', []);
Expand All @@ -737,7 +741,7 @@ public function testAttributes()
$requestStack->push($request);

$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
$serializerContextBuilderProphecy->createFromRequest($request, true)->shouldBeCalled()->willReturn([AbstractNormalizer::GROUPS => ['foo'], AbstractNormalizer::ATTRIBUTES => ['relatedDummy' => ['id', 'name']]]);
$serializerContextBuilderProphecy->createFromRequest($request, true)->shouldBeCalled()->willReturn([AbstractNormalizer::GROUPS => ['foo'], AbstractNormalizer::ATTRIBUTES => ['relatedDummies' => ['id', 'name'], 'relatedDummy' => ['id', 'name']]]);

$queryBuilder = $queryBuilderProphecy->reveal();
$eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30, false, $requestStack, $serializerContextBuilderProphecy->reveal(), true);
Expand Down
Expand Up @@ -1339,6 +1339,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
'api_platform.metadata.property_schema.one_of_restriction',
'api_platform.metadata.property_schema.regex_restriction',
'api_platform.metadata.property_schema.format_restriction',
'api_platform.metadata.property_schema.unique_restriction',
'api_platform.metadata.property.metadata_factory.yaml',
'api_platform.metadata.property.name_collection_factory.yaml',
'api_platform.metadata.resource.filter_metadata_factory.annotation',
Expand Down
30 changes: 29 additions & 1 deletion tests/Bridge/Symfony/Routing/RouterTest.php
Expand Up @@ -96,7 +96,7 @@ public function testMatch()
$this->assertEquals(['bar'], $router->match('/app_dev.php/foo'));
}

public function testWithinvalidContext()
public function testMatchWithInvalidContext()
{
$this->expectException(RoutingExceptionInterface::class);
$this->expectExceptionMessage('Invalid request context.');
Expand All @@ -108,4 +108,32 @@ public function testWithinvalidContext()
$router = new Router($mockedRouter->reveal());
$router->match('28-01-2018 10:10');
}

public function testMatchDuplicatedBaseUrl()
{
$context = new RequestContext('/app', 'GET', 'localhost', 'https');

$mockedRouter = $this->prophesize(RouterInterface::class);
$mockedRouter->getContext()->willReturn($context);
$mockedRouter->setContext(Argument::type(RequestContext::class))->willReturn();
$mockedRouter->match('/api/app_crm/resource')->willReturn(['bar']);

$router = new Router($mockedRouter->reveal());

$this->assertEquals(['bar'], $router->match('/app/api/app_crm/resource'));
}

public function testMatchEmptyBaseUrl()
{
$context = new RequestContext('', 'GET', 'localhost', 'https');

$mockedRouter = $this->prophesize(RouterInterface::class);
$mockedRouter->getContext()->willReturn($context);
$mockedRouter->setContext(Argument::type(RequestContext::class))->willReturn();
$mockedRouter->match('/foo')->willReturn(['bar']);

$router = new Router($mockedRouter->reveal());

$this->assertEquals(['bar'], $router->match('/foo'));
}
}
@@ -0,0 +1,57 @@
<?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\Tests\Bridge\Symfony\Validator\Metadata\Property\Restriction;

use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaUniqueRestriction;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Tests\ProphecyTrait;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Positive;
use Symfony\Component\Validator\Constraints\Unique;

/**
* @author Tomas Norkūnas <norkunas.tom@gmail.com>
*/
final class PropertySchemaUniqueRestrictionTest extends TestCase
{
use ProphecyTrait;

private $propertySchemaUniqueRestriction;

protected function setUp(): void
{
$this->propertySchemaUniqueRestriction = new PropertySchemaUniqueRestriction();
}

/**
* @dataProvider supportsProvider
*/
public function testSupports(Constraint $constraint, PropertyMetadata $propertyMetadata, bool $expectedResult): void
{
self::assertSame($expectedResult, $this->propertySchemaUniqueRestriction->supports($constraint, $propertyMetadata));
}

public function supportsProvider(): \Generator
{
yield 'supported' => [new Unique(), new PropertyMetadata(), true];

yield 'not supported' => [new Positive(), new PropertyMetadata(), false];
}

public function testCreate(): void
{
self::assertSame(['uniqueItems' => true], $this->propertySchemaUniqueRestriction->create(new Unique(), new PropertyMetadata()));
}
}

0 comments on commit 5844f64

Please sign in to comment.