From 3567025d3245214f97f9ac6635d37646cfdc4574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 27 May 2014 15:28:09 +0200 Subject: [PATCH 1/3] Add support for OneToOne --- .../Matcher/AssociationSubPathMatcher.php | 12 ++-- .../Matcher/AssociationSubPathMatcherTest.php | 61 +++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php b/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php index 70062a2..4ff9c1b 100755 --- a/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php +++ b/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php @@ -83,10 +83,12 @@ public function matchSubPath(ResourceInterface $resource, $subPath, SubPathMatch $associationData = $reflectionProperty->getValue($resource->getData()); - return new SubPathMatch( - new Resource($associationData, $associationResourceMetadata), - $associationPath, - $previousMatch - ); + if ($associationData === null && $classMetadata->isSingleValuedAssociation($associationName)) { + $resource = $associationResourceMetadata->createResource(); + } else { + $resource = new Resource($associationData, $associationResourceMetadata); + } + + return new SubPathMatch($resource, $associationPath, $previousMatch); } } diff --git a/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php b/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php index 1e13c77..4b62fc1 100644 --- a/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php +++ b/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php @@ -180,4 +180,65 @@ public function testWontMatchWhenRoutableIsSetToFalse() $this->assertNull($this->associationMatcher->matchSubPath($resource, 'bar')); } + + public function testCanCreateEmptyResourceForSingleValuedAssociation() + { + $resource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $metadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + + $data = new AssociationMatcherEntity(); + + $resource->expects($this->once())->method('getData')->will($this->returnValue($data)); + $resource->expects($this->once())->method('getMetadata')->will($this->returnValue($metadata)); + $metadata->expects($this->once()) + ->method('hasAssociationMetadata') + ->with('card') + ->will($this->returnValue(true)); + + $metadata->expects($this->once()) + ->method('getAssociationMetadata') + ->with('card') + ->will($this->returnValue([ + 'routable' => true, + 'propertyName' => 'card', + 'path' => 'card' + ])); + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $classMetadata->expects($this->once()) + ->method('getAssociationTargetClass') + ->with('card') + ->will($this->returnValue('Card')); + + $associationMetadata = $this->getMock('ZfrRest\Resource\Metadata\ResourceMetadataInterface'); + $associationMetadata->expects($this->once()) + ->method('createResource') + ->will($this->returnValue($this->getMock('ZfrRest\Resource\ResourceInterface'))); + + $this->metadataFactory->expects($this->once()) + ->method('getMetadataForClass') + ->with('Card') + ->will($this->returnValue($associationMetadata)); + + $reflectionProperty = $this->getMock('ReflectionProperty', [], [], '', false); + $reflectionProperty->expects($this->once())->method('getValue')->will($this->returnValue(null)); + + $reflectionClass = $this->getMock('ReflectionClass', [], [], '', false); + $reflectionClass->expects($this->once())->method('getProperty')->will($this->returnValue($reflectionProperty)); + + $metadata->expects($this->once()) + ->method('getReflectionClass') + ->will($this->returnValue($reflectionClass)); + + $metadata->expects($this->once())->method('getClassMetadata')->will($this->returnValue($classMetadata)); + $classMetadata->expects($this->once()) + ->method('isSingleValuedAssociation') + ->with('card') + ->will($this->returnValue(true)); + + $result = $this->associationMatcher->matchSubPath($resource, 'card'); + + $this->assertInstanceOf('ZfrRest\Router\Http\Matcher\SubPathMatch', $result); + $this->assertInstanceOf('ZfrRest\Resource\ResourceInterface', $result->getMatchedResource()); + } } From 477c6f5aa20b84db2d4c071f326cf7c2353aef1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Tue, 27 May 2014 16:27:47 +0200 Subject: [PATCH 2/3] Add terminal path --- CHANGELOG.md | 5 ++++ .../Matcher/AssociationSubPathMatcher.php | 7 +++++- .../Http/Matcher/BaseSubPathMatcher.php | 2 +- .../Router/Http/Matcher/SubPathMatch.php | 25 +++++++++++++++++-- .../Matcher/AssociationSubPathMatcherTest.php | 2 ++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4086fbf..f90780f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## 0.3.2 + +* Added support for OneToOne association (assuming you have a "User" entity with a OneToOne association to +a "Card" entity, you can now do a POST request like "/users/4/card") + ## 0.3.1 * ZfrRest now returns input errors correctly if no data was given in the body diff --git a/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php b/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php index 4ff9c1b..4932b90 100755 --- a/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php +++ b/src/ZfrRest/Router/Http/Matcher/AssociationSubPathMatcher.php @@ -82,13 +82,18 @@ public function matchSubPath(ResourceInterface $resource, $subPath, SubPathMatch $reflectionProperty->setAccessible(true); $associationData = $reflectionProperty->getValue($resource->getData()); + $terminal = false; if ($associationData === null && $classMetadata->isSingleValuedAssociation($associationName)) { $resource = $associationResourceMetadata->createResource(); + + // We set this match as terminal, so that paths like "/user/4/twitter/tweets/123" don't end + // up creating a lot of "non-existant" resources, that would be very strange to handle + $terminal = true; } else { $resource = new Resource($associationData, $associationResourceMetadata); } - return new SubPathMatch($resource, $associationPath, $previousMatch); + return new SubPathMatch($resource, $associationPath, $previousMatch, $terminal); } } diff --git a/src/ZfrRest/Router/Http/Matcher/BaseSubPathMatcher.php b/src/ZfrRest/Router/Http/Matcher/BaseSubPathMatcher.php index 091a284..89952d5 100755 --- a/src/ZfrRest/Router/Http/Matcher/BaseSubPathMatcher.php +++ b/src/ZfrRest/Router/Http/Matcher/BaseSubPathMatcher.php @@ -59,7 +59,7 @@ public function matchSubPath(ResourceInterface $resource, $subPath, SubPathMatch $subPath = trim($subPath, '/'); // We have traversed the whole path, return the last matched path! - if (empty($subPath)) { + if (empty($subPath) || ($previousMatch && $previousMatch->isTerminal())) { return $previousMatch ?: new SubPathMatch($resource, $subPath); } diff --git a/src/ZfrRest/Router/Http/Matcher/SubPathMatch.php b/src/ZfrRest/Router/Http/Matcher/SubPathMatch.php index 3d24be7..548048d 100755 --- a/src/ZfrRest/Router/Http/Matcher/SubPathMatch.php +++ b/src/ZfrRest/Router/Http/Matcher/SubPathMatch.php @@ -44,16 +44,27 @@ class SubPathMatch */ protected $previousMatch; + /** + * @var bool + */ + protected $terminal; + /** * @param ResourceInterface $matchedResource * @param string $matchedPath * @param SubPathMatch|null $previousMatch + * @param bool $terminal */ - public function __construct(ResourceInterface $matchedResource, $matchedPath, SubPathMatch $previousMatch = null) - { + public function __construct( + ResourceInterface $matchedResource, + $matchedPath, + SubPathMatch $previousMatch = null, + $terminal = false + ) { $this->matchedResource = $matchedResource; $this->matchedPath = $matchedPath; $this->previousMatch = $previousMatch; + $this->terminal = (bool) $terminal; } /** @@ -85,4 +96,14 @@ public function getPreviousMatch() { return $this->previousMatch; } + + /** + * Get if this sub path match is a terminal path + * + * @return bool + */ + public function isTerminal() + { + return $this->terminal; + } } diff --git a/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php b/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php index 4b62fc1..fe2e6dc 100644 --- a/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php +++ b/tests/ZfrRestTest/Router/Http/Matcher/AssociationSubPathMatcherTest.php @@ -160,6 +160,7 @@ public function testCanMatchAssociation($subPath, $associationPath, $propertyNam $this->assertSame($associationMetadata, $result->getMatchedResource()->getMetadata()); $this->assertEquals($associationPath, $result->getMatchedPath()); $this->assertNull($result->getPreviousMatch()); + $this->assertFalse($result->isTerminal()); } public function testWontMatchWhenRoutableIsSetToFalse() @@ -240,5 +241,6 @@ public function testCanCreateEmptyResourceForSingleValuedAssociation() $this->assertInstanceOf('ZfrRest\Router\Http\Matcher\SubPathMatch', $result); $this->assertInstanceOf('ZfrRest\Resource\ResourceInterface', $result->getMatchedResource()); + $this->assertTrue($result->isTerminal()); } } From 202976c49312c0640e2909615af78691eb2712be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Thu, 29 May 2014 23:03:21 +0200 Subject: [PATCH 3/3] Add test --- .../Router/Http/Matcher/BaseSubPathMatcherTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/ZfrRestTest/Router/Http/Matcher/BaseSubPathMatcherTest.php b/tests/ZfrRestTest/Router/Http/Matcher/BaseSubPathMatcherTest.php index fd3e4fd..b43fa04 100644 --- a/tests/ZfrRestTest/Router/Http/Matcher/BaseSubPathMatcherTest.php +++ b/tests/ZfrRestTest/Router/Http/Matcher/BaseSubPathMatcherTest.php @@ -129,4 +129,18 @@ public function testCanMatch($subPath) $this->assertSame($secondMatch, $result); $this->assertSame($secondMatchedResource, $result->getMatchedResource()); } + + public function testReturnsMatchIfTerminalEvenIfPathIsNotFullyConsumed() + { + $baseResource = $this->getMock('ZfrRest\Resource\ResourceInterface'); + $previousMatch = $this->getMock('ZfrRest\Router\Http\Matcher\SubPathMatch', [], [], '', false); + $previousMatch->expects($this->once())->method('isTerminal')->will($this->returnValue(true)); + + $this->associationMatcher->expects($this->never())->method('matchSubPath'); + $this->collectionMatcher->expects($this->never())->method('matchSubPath'); + + $result = $this->baseMatcher->matchSubPath($baseResource, '/tweets/123', $previousMatch); + + $this->assertSame($result, $previousMatch); + } }