diff --git a/src/Model/Context.php b/src/Model/Context.php new file mode 100644 index 0000000..ae55b05 --- /dev/null +++ b/src/Model/Context.php @@ -0,0 +1,91 @@ +group; + } + + /** + * @return $this + */ + public function setGroup(?string $group): self + { + $this->group = $group; + return $this; + } + + public function getIncludes(): array + { + return $this->includes; + } + + /** + * @return $this + */ + public function setIncludes(array $includes): self + { + $this->includes = $includes; + + return $this; + } + + /** + * @return $this + */ + public function setIncludesFromString(string $includes): self + { + $this->setIncludes(explode(',', $includes)); + + return $this; + } + + /** + * @return $this + */ + public function addInclude(string $include): self + { + $this->includes[] = $include; + + return $this; + } + + public function hasInclude(string $assertion): bool + { + return in_array($assertion, $this->includes); + } + + public function hasIncludes(): bool + { + return count($this->includes) > 0; + } + + public function getMaxDepth(): ?int + { + return $this->maxDepth; + } + + /** + * @return $this + */ + public function setMaxDepth(int $maxDepth): self + { + $this->maxDepth = $maxDepth; + + return $this; + } +} \ No newline at end of file diff --git a/src/Service/Normalize/AbstractNormalizer.php b/src/Service/Normalize/AbstractNormalizer.php index 482238c..426830a 100644 --- a/src/Service/Normalize/AbstractNormalizer.php +++ b/src/Service/Normalize/AbstractNormalizer.php @@ -7,16 +7,20 @@ use BowlOfSoup\NormalizerBundle\Annotation\Normalize; use BowlOfSoup\NormalizerBundle\Annotation\Translate; use BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException; +use BowlOfSoup\NormalizerBundle\Model\Context; use BowlOfSoup\NormalizerBundle\Model\ObjectCache; use BowlOfSoup\NormalizerBundle\Service\Extractor\AnnotationExtractor; use BowlOfSoup\NormalizerBundle\Service\Extractor\ClassExtractor; -use BowlOfSoup\NormalizerBundle\Service\Normalizer; use BowlOfSoup\NormalizerBundle\Service\ObjectHelper; use Doctrine\Common\Collections\Collection; use Symfony\Contracts\Translation\TranslatorInterface; abstract class AbstractNormalizer { + protected const TYPE_DATETIME = 'datetime'; + protected const TYPE_OBJECT = 'object'; + protected const TYPE_COLLECTION = 'collection'; + /** @var \BowlOfSoup\NormalizerBundle\Service\Normalizer|null */ protected $sharedNormalizer = null; @@ -32,6 +36,12 @@ abstract class AbstractNormalizer /** @var string|null */ protected $group = null; + /** @var \BowlOfSoup\NormalizerBundle\Model\Context */ + protected $context; + + /** @var array */ + protected $currentPath = []; + /** @var int|null */ protected $maxDepth = null; @@ -56,6 +66,7 @@ public function __construct( public function cleanUp(): void { + $this->currentPath = []; $this->maxDepth = null; } @@ -75,6 +86,26 @@ protected function hasMaxDepth(): bool return null !== $this->maxDepth && ($this->processedDepth + 1) > $this->maxDepth; } + /** + * This function deals with processing $context, which due to compatibility can be either a group or a context model. + * Refactor this in the next major version. + * + * @param \BowlOfSoup\NormalizerBundle\Model\Context|string|null $context + */ + protected function handleContext($context): void + { + if (is_string($context)) { + // Group has been given instead of context. Set group on context. + $context = (new Context())->setGroup($context); + } elseif (!$context instanceof Context) { + // No context has been given, instantiate empty context. + $context = new Context(); + } + + $this->group = $context->getGroup(); + $this->context = $context; + } + /** * @throws \BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException * @@ -138,7 +169,7 @@ protected function normalizeReferencedObject(object $object, object $parentObjec $objectName = get_class($object); if (is_object($object) && !$this->isCircularReference($object, $objectName)) { - $normalizedConstruct = $this->sharedNormalizer->normalizeObject($object, $this->group); + $normalizedConstruct = $this->sharedNormalizer->normalizeObject($object, $this->context); if (empty($normalizedConstruct)) { return null; @@ -194,10 +225,11 @@ protected function normalizeReferencedCollection($propertyValue, Normalize $prop $propertyAnnotation ); } else { - $normalizedObject = $this->sharedNormalizer->normalizeObject($collectionItem, $this->group); + $normalizedObject = $this->sharedNormalizer->normalizeObject($collectionItem, $this->context); $normalizedCollection[] = (!empty($normalizedObject) ? $normalizedObject : null); } --$this->processedDepth; + } return $normalizedCollection; @@ -226,7 +258,7 @@ protected function handleCallbackResult($propertyValue, Normalize $propertyAnnot $allObjects = false; continue; } - $normalizedCollection[] = $this->sharedNormalizer->normalizeObject($item, $this->group); + $normalizedCollection[] = $this->sharedNormalizer->normalizeObject($item, $this->context); } if (empty($normalizedCollection) && !$allObjects) { return $propertyValue; @@ -234,7 +266,7 @@ protected function handleCallbackResult($propertyValue, Normalize $propertyAnnot return $normalizedCollection; } elseif (is_object($propertyValue)) { - return $this->sharedNormalizer->normalizeObject($propertyValue, $this->group); + return $this->sharedNormalizer->normalizeObject($propertyValue, $this->context); } return $propertyValue; @@ -303,6 +335,34 @@ protected function storeNormalizedConstructForObject(string $constructName, stri $this->nameAndClassStore[$baseObjectName]->set($constructName, $object); } + protected function decreaseCurrentPath(): void + { + if (is_array($this->currentPath) && count($this->currentPath) > 1) { + array_pop($this->currentPath); + } else { + $this->currentPath = []; + } + } + + protected function canCurrentPathBeIncluded(?string $pathType): bool + { + if ($pathType !== static::TYPE_OBJECT && $pathType !== static::TYPE_COLLECTION) { + return true; + } + + if ($this->context->getMaxDepth() === (count($this->currentPath) - 1)) { + return false; + } + + if (!$this->context->hasIncludes()) { + return true; + } + + $pathThatIsBeingNormalized = implode('.', $this->currentPath); + + return $this->context->hasInclude($pathThatIsBeingNormalized); + } + private function isCircularReference(object $object, string $objectName): bool { $objectIdentifier = ObjectHelper::getObjectIdentifier($object); diff --git a/src/Service/Normalize/MethodNormalizer.php b/src/Service/Normalize/MethodNormalizer.php index 5e978e4..31227f0 100644 --- a/src/Service/Normalize/MethodNormalizer.php +++ b/src/Service/Normalize/MethodNormalizer.php @@ -31,20 +31,21 @@ public function __construct( } /** + * @param \BowlOfSoup\NormalizerBundle\Model\Context|string|null $context + * * @throws \BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException * @throws \ReflectionException */ public function normalize( Normalizer $sharedNormalizer, ObjectBag $objectBag, - ?string $group + $context ): array { $object = $objectBag->getObject(); $objectName = $objectBag->getObjectName(); $this->sharedNormalizer = $sharedNormalizer; - $this->group = $group; - + $this->handleContext($context); $this->processedDepthObjects[$objectName] = $this->processedDepth; $normalizedMethods = []; @@ -104,6 +105,19 @@ private function normalizeMethod( continue; } + $annotationName = $methodAnnotation->getName(); + if (!empty($annotationName)) { + $methodName = $methodAnnotation->getName(); + } + + // Add to current path, like a breadcrumb where we are when normalizing. + $this->currentPath[] = $methodName; + if (!$this->canCurrentPathBeIncluded($methodAnnotation->getType())) { + $this->decreaseCurrentPath(); + + continue; + } + if ($methodAnnotation->hasType()) { $methodValue = $this->getValueForMethodWithType( $object, @@ -120,17 +134,14 @@ private function normalizeMethod( } } - $annotationName = $methodAnnotation->getName(); - if (!empty($annotationName)) { - $methodName = $methodAnnotation->getName(); - } - $methodValue = (is_array($methodValue) && empty($methodValue) ? null : $methodValue); if (null !== $translationAnnotation) { $methodValue = $this->translateValue($methodValue, $translationAnnotation); } $normalizedProperties[$methodName] = $methodValue; + + $this->decreaseCurrentPath(); } return $normalizedProperties; @@ -156,11 +167,11 @@ private function getValueForMethodWithType( $newMethodValue = null; $annotationMethodType = strtolower($annotationMethodType); - if ('datetime' === $annotationMethodType) { + if (static::TYPE_DATETIME === $annotationMethodType) { $newMethodValue = $this->getValueForMethodWithDateTime($object, $method, $methodAnnotation); - } elseif ('object' === $annotationMethodType) { + } elseif (static::TYPE_OBJECT === $annotationMethodType) { $newMethodValue = $this->getValueForMethodWithTypeObject($object, $method, $methodValue, $methodAnnotation); - } elseif ('collection' === $annotationMethodType) { + } elseif (static::TYPE_COLLECTION === $annotationMethodType) { $newMethodValue = $this->normalizeReferencedCollection($methodValue, $methodAnnotation); } diff --git a/src/Service/Normalize/PropertyNormalizer.php b/src/Service/Normalize/PropertyNormalizer.php index 83d0eff..4e5fe66 100644 --- a/src/Service/Normalize/PropertyNormalizer.php +++ b/src/Service/Normalize/PropertyNormalizer.php @@ -32,20 +32,22 @@ public function __construct( } /** + * @param \BowlOfSoup\NormalizerBundle\Model\Context|string|null $context + * * @throws \BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException * @throws \ReflectionException */ public function normalize( Normalizer $sharedNormalizer, ObjectBag $objectBag, - ?string $group + $context ): array { $object = $objectBag->getObject(); $objectName = $objectBag->getObjectName(); $objectIdentifier = $objectBag->getObjectIdentifier(); $this->sharedNormalizer = $sharedNormalizer; - $this->group = $group; + $this->handleContext($context); $this->nameAndClassStore[$objectIdentifier] = new Store(); $normalizedProperties = []; @@ -123,6 +125,19 @@ private function normalizeProperty( continue; } + $annotationName = $propertyAnnotation->getName(); + if (!empty($annotationName)) { + $propertyName = $propertyAnnotation->getName(); + } + + // Add to current path, like a breadcrumb where we are when normalizing. + $this->currentPath[] = $propertyName; + if (!$this->canCurrentPathBeIncluded($propertyAnnotation->getType())) { + $this->decreaseCurrentPath(); + + continue; + } + if ($propertyAnnotation->hasType()) { $propertyValue = $this->getValueForPropertyWithType( $object, @@ -142,17 +157,14 @@ private function normalizeProperty( } } - $annotationName = $propertyAnnotation->getName(); - if (!empty($annotationName)) { - $propertyName = $propertyAnnotation->getName(); - } - $propertyValue = (is_array($propertyValue) && empty($propertyValue) ? null : $propertyValue); if (null !== $translationAnnotation) { $propertyValue = $this->translateValue($propertyValue, $translationAnnotation); } $normalizedProperties[$propertyName] = $propertyValue; + + $this->decreaseCurrentPath(); } return $normalizedProperties; @@ -178,11 +190,11 @@ private function getValueForPropertyWithType( $newPropertyValue = null; $annotationPropertyType = strtolower($annotationPropertyType); - if ('datetime' === $annotationPropertyType) { + if (static::TYPE_DATETIME === $annotationPropertyType) { $newPropertyValue = $this->getValueForPropertyWithDateTime($object, $property, $propertyAnnotation); - } elseif ('object' === $annotationPropertyType) { + } elseif (static::TYPE_OBJECT === $annotationPropertyType) { $newPropertyValue = $this->getValueForPropertyWithTypeObject($object, $propertyValue, $propertyAnnotation); - } elseif ('collection' === $annotationPropertyType) { + } elseif (static::TYPE_COLLECTION === $annotationPropertyType) { $newPropertyValue = $this->normalizeReferencedCollection($propertyValue, $propertyAnnotation); } diff --git a/src/Service/Normalizer.php b/src/Service/Normalizer.php index 3b97145..c8048fe 100644 --- a/src/Service/Normalizer.php +++ b/src/Service/Normalizer.php @@ -5,6 +5,7 @@ namespace BowlOfSoup\NormalizerBundle\Service; use BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException; +use BowlOfSoup\NormalizerBundle\Model\Context; use BowlOfSoup\NormalizerBundle\Model\ObjectBag; use BowlOfSoup\NormalizerBundle\Model\ObjectCache; use BowlOfSoup\NormalizerBundle\Service\Extractor\ClassExtractor; @@ -36,28 +37,39 @@ public function __construct( * Normalize an object or an array of objects, for a specific group. * * @param mixed $data + * @param \BowlOfSoup\NormalizerBundle\Model\Context|string|null $context * * @throws \BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException * @throws \ReflectionException */ - public function normalize($data, string $group = null): array + public function normalize($data, $context = null): array { if (empty($data)) { return []; } + if (is_string($context)) { + // Group has been given instead of context. Set group on context. + $context = (new Context())->setGroup($context); + } elseif (!$context instanceof Context) { + // No context has been given, instantiate empty context. + $context = new Context(); + } + $this->cleanUpSession(); - return $this->normalizeData($data, $group); + return $this->normalizeData($data, $context); } /** * Get properties for given object, annotations per property and begin normalizing. * + * @param \BowlOfSoup\NormalizerBundle\Model\Context|string $context + * * @throws \ReflectionException * @throws \BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException */ - public function normalizeObject(object $object, ?string $group): array + public function normalizeObject(object $object, $context): array { $normalizedConstructs = []; $objectName = get_class($object); @@ -73,10 +85,10 @@ public function normalizeObject(object $object, ?string $group): array $objectBag = new ObjectBag($object, $objectIdentifier, $objectName); - $normalizedClassProperties = $this->propertyNormalizer->normalize($this, $objectBag, $group); + $normalizedClassProperties = $this->propertyNormalizer->normalize($this, $objectBag, $context); $normalizedConstructs = array_merge($normalizedConstructs, ...$normalizedClassProperties); - $normalizedClassMethods = $this->methodNormalizer->normalize($this, $objectBag, $group); + $normalizedClassMethods = $this->methodNormalizer->normalize($this, $objectBag, $context); $normalizedConstructs = array_merge($normalizedConstructs, ...$normalizedClassMethods); if (null !== $objectIdentifier) { @@ -89,20 +101,22 @@ public function normalizeObject(object $object, ?string $group): array } /** + * @param \BowlOfSoup\NormalizerBundle\Model\Context|string $context + * * @throws \BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException * @throws \ReflectionException */ - private function normalizeData($data, ?string $group): array + private function normalizeData($data, $context): array { $this->propertyNormalizer->cleanUp(); $normalizedData = []; if (is_iterable($data) || $data instanceof \Traversable) { foreach ($data as $item) { - $normalizedData[] = $this->normalizeData($item, $group); + $normalizedData[] = $this->normalizeData($item, $context); } } elseif (is_object($data)) { - $normalizedData = $this->normalizeObject($data, $group); + $normalizedData = $this->normalizeObject($data, $context); } else { throw new BosNormalizerException('Can only normalize an object or an array of objects. Input contains: ' . gettype($data)); } diff --git a/tests/Service/NormalizerTest.php b/tests/Service/NormalizerTest.php index f943a4d..15c9e9f 100644 --- a/tests/Service/NormalizerTest.php +++ b/tests/Service/NormalizerTest.php @@ -5,8 +5,8 @@ namespace BowlOfSoup\NormalizerBundle\Tests\Service; use BowlOfSoup\NormalizerBundle\Exception\BosNormalizerException; +use BowlOfSoup\NormalizerBundle\Model\Context; use BowlOfSoup\NormalizerBundle\Service\Extractor\ClassExtractor; -use BowlOfSoup\NormalizerBundle\Service\Normalizer; use BowlOfSoup\NormalizerBundle\Tests\ArraySubset; use BowlOfSoup\NormalizerBundle\Tests\assets\Address; use BowlOfSoup\NormalizerBundle\Tests\assets\Group; @@ -99,18 +99,94 @@ public function testNormalizeSuccessDifferentGroup(): void 'surName' => 'Of Soup', 'addresses' => [ [ + 'street' => 'Dummy Street', + 'number' => null, + 'postalCode' => null, + 'city' => 'Amsterdam', 'getSpecialNotesForDelivery' => 'some special string', ], [ + 'street' => null, + 'number' => '4', + 'postalCode' => '1234AB', + 'city' => null, 'getSpecialNotesForDelivery' => 'some special string', ], ], + 'hobbies' => [ + [ + 'id' => 1, + ], + [ + 'id' => 2, + ], + [ + 'id' => 3, + ], + ] ]; $this->assertNotEmpty($result); ArraySubset::assert($result, $expectedResult); } + /** + * @testdox Normalize object, with includes set. + */ + public function testNormalizeSuccessWithIncludes(): void + { + $person = $this->getDummyDataSet(); + + $result = $this->normalizer->normalize($person, (new Context()) + ->setGroup('anotherGroup') + ->setIncludesFromString('addresses') + ); + + $expectedResult = [ + 'surName' => 'Of Soup', + 'addresses' => [ + [ + 'street' => 'Dummy Street', + 'number' => null, + 'postalCode' => null, + 'city' => 'Amsterdam', + 'getSpecialNotesForDelivery' => 'some special string', + ], + [ + 'street' => null, + 'number' => 4, + 'postalCode' => '1234AB', + 'city' => null, + 'getSpecialNotesForDelivery' => 'some special string', + ], + ], + ]; + + $this->assertNotEmpty($result); + $this->assertSame($expectedResult, $result); + } + + /** + * @testdox Normalize object, with includes set and max include depth + */ + public function testNormalizeSuccessWithIncludesAndDepth(): void + { + $person = $this->getDummyDataSet(); + + $result = $this->normalizer->normalize($person, (new Context()) + ->setGroup('anotherGroup') + ->setIncludesFromString('addresses') + ->setMaxDepth(0) + ); + + $expectedResult = [ + 'surName' => 'Of Soup', + ]; + + $this->assertNotEmpty($result); + $this->assertSame($expectedResult, $result); + } + /** * @testdox Normalize object, normalize with no group specified. */ diff --git a/tests/assets/Address.php b/tests/assets/Address.php index 65f18b2..53e0ceb 100644 --- a/tests/assets/Address.php +++ b/tests/assets/Address.php @@ -13,6 +13,7 @@ class Address * @var string * * @Bos\Normalize(group={"default"}) + * @Bos\Normalize(group={"anotherGroup"}) */ private $street; @@ -20,6 +21,7 @@ class Address * @var int * * @Bos\Normalize(group={"default"}) + * @Bos\Normalize(group={"anotherGroup"}) */ private $number; @@ -27,6 +29,7 @@ class Address * @var string * * @Bos\Normalize(group={"default"}) + * @Bos\Normalize(group={"anotherGroup"}) */ private $postalCode; @@ -34,6 +37,7 @@ class Address * @var string * * @Bos\Normalize(group={"default"}, callback="getCityWithFormat") + * @Bos\Normalize(group={"anotherGroup"}) */ private $city; diff --git a/tests/assets/Hobbies.php b/tests/assets/Hobbies.php index 3989889..99dc55d 100644 --- a/tests/assets/Hobbies.php +++ b/tests/assets/Hobbies.php @@ -10,6 +10,7 @@ class Hobbies { /** * @Bos\Normalize(group={"default", "duplicateObjectId"}) + * @Bos\Normalize(group={"anotherGroup"}) * * @var int|null */ diff --git a/tests/assets/Person.php b/tests/assets/Person.php index 5a6abad..0ef7942 100644 --- a/tests/assets/Person.php +++ b/tests/assets/Person.php @@ -107,6 +107,7 @@ class Person extends AbstractPerson * @var \BowlOfSoup\NormalizerBundle\Tests\assets\Hobbies[] * * @Bos\Normalize(group={"default", "duplicateObjectId"}, type="collection") + * @Bos\Normalize(group={"anotherGroup"}, type="collection") */ private $hobbies;