diff --git a/docs/local_data.md b/docs/local_data.md new file mode 100644 index 0000000000000000000000000000000000000000..9602140eccef940ae4b0204f87325cd729f7c954 --- /dev/null +++ b/docs/local_data.md @@ -0,0 +1,65 @@ +#Local Data + +Local data provides a mechanism to extend base-entities by attributes which are not part of the entities default set of attributes. Local data can be added in custom base-entity (post-)event subscribers. + +## Local Data requests + +Local data can be requested using the `inlucde` parameter provided by base-entity GET operations by default. The format is the following: + +```php +include=<ResourceName>.<attributeName>,... +``` + +It is a comma-separated list of 0 ... n `<ResourceName>.<attributeName>` pairs. Note that `ResourceName` is the `shortName` defined in the `ApiResource` annotation of an entity. The backend will return an error if +* The format of the `include` parameter is invalid +* Any of the requested attributes could not be provided +* The backend tries to set an attribute which was not requested + +##Adding local data attributes + +Integraters have to make sure that local attributes requested by their client applications are added in the backend. This can be done in custom base-entity event subscribers. + +```php +class BaseEntityEventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + BaseEntityPostEvent::NAME => 'onPost', + ]; + } + + public function onPost(BaseEntityPostEvent $event) + { + $data = $event->getBaseEntityData(); + if ($event->isLocalDataAttributeRequested('foo')) { + $event->setLocalDataAttribute('foo', $data->getFoo()); + } + } +} +``` + +##Creating Local Data aware Entities + +You can easily add Local Data to your Entity (`MyEntity`) by: + +* Using the `LocalDataAwareTrait` in `MyEntity` +* Implementing the `LocalDataAwareInterface` in `MyEntity` +* Adding the `LocalData:output` group to the normalization context of `MyEntity` +* Adding an event dispatcher of type `LocalDataAwareEventDispatcher` to your Entity provider +* On GET-requests, passing the value of the `include` parameter to the event dispatcher +```php +$this->eventDispatcher->initRequestedLocalDataAttributes($includeParameter); +``` +* Creating a (post-)event `MyEntityPostEvent` extending the `LocalDataAwareEvent`, which you pass to the event dispatcher once your Entity provider is done setting up a new instance of `MyEntity`: +```php +// get some data +$myEntityData = $externalApi->getEntityData($identifier); +$myEntity = new MyEntity(); +// first, set the default attributes: +$myEntity->setIdentifier($myEntityData->getIdentifier()); +$myEntity->setName($myEntityData->getName()); +// now, for custom attributes: +$postEvent = new MyEntityPostEvent($myEntity, $myEntityData); +$this->eventDispatcher->dispatch($postEvent); +``` \ No newline at end of file diff --git a/src/Entity/LocalDataAwareInterface.php b/src/Entity/LocalDataAwareInterface.php index 9aa8daf27965142ed64409445a137e5779f48a32..5fc64a25ddf98b9feb26fb995787fbfada095f57 100644 --- a/src/Entity/LocalDataAwareInterface.php +++ b/src/Entity/LocalDataAwareInterface.php @@ -6,22 +6,20 @@ namespace Dbp\Relay\CoreBundle\Entity; interface LocalDataAwareInterface { - /** - * Returns the unique name (shortName of the ApiResource) of this entity. - */ - public static function getUniqueEntityName(): string; - /** * Sets the value of a local data attribute. * - * @param mixed|null $value + * @param string $key the attribute name + * @param ?mixed $value the attribute value */ public function setLocalDataValue(string $key, $value): void; /** - * Returns the value of local data value attribute or null if the attribute is not found. + * Returns the value of a local data attribute. + * + * @param string $key The attribute name * - * @return ?mixed + * @return ?mixed The value or null if the attribute is not found */ public function getLocalDataValue(string $key); } diff --git a/src/Entity/LocalDataAwareTrait.php b/src/Entity/LocalDataAwareTrait.php index e2cd56b43635232da4741e6cef04a655a2e6a2e4..edf94aaa05cd00172ecf44fcbea560a245033a44 100644 --- a/src/Entity/LocalDataAwareTrait.php +++ b/src/Entity/LocalDataAwareTrait.php @@ -18,15 +18,19 @@ trait LocalDataAwareTrait */ private $localData; + /** + * Returns the array of local data attributes. + */ public function getLocalData(): array { return $this->localData; } /** - * Adds a local data entry. + * Sets the value of a local data attribute. * - * @param mixed|null $value + * @param string $key the attribute name + * @param mixed|null $value the attribute value */ public function setLocalDataValue(string $key, $value): void { @@ -38,9 +42,11 @@ trait LocalDataAwareTrait /** * @Ignore - * Returns the local data value for the given key or null if the key is not found. + * Returns the value of a local data attribute. + * + * @param string $key the attribute name * - * @return ?mixed + * @return ?mixed the value or null if the attribute is not found */ public function getLocalDataValue(string $key) { diff --git a/src/Event/LocalDataAwareEvent.php b/src/Event/LocalDataAwareEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..1b5b474525cfd33bf4ac82acb6591118f3cd42e1 --- /dev/null +++ b/src/Event/LocalDataAwareEvent.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\Event; + +use Dbp\Relay\CoreBundle\Entity\LocalDataAwareInterface; +use Dbp\Relay\CoreBundle\Exception\ApiError; +use Dbp\Relay\CoreBundle\Service\LocalDataAwareEventDispatcher; +use Symfony\Contracts\EventDispatcher\Event; + +class LocalDataAwareEvent extends Event +{ + /** @var LocalDataAwareInterface */ + private $entity; + + /** @var array */ + private $requestedAttributes; + + protected function __construct(LocalDataAwareInterface $entity) + { + $this->entity = $entity; + } + + /** + * Sets the list of requested local data attribute names for this event's entity. + * + * @param string[] $requestedAttributes + */ + public function setRequestedAttributes(array $requestedAttributes): void + { + $this->requestedAttributes = $requestedAttributes; + } + + /** + * Returns the list of local data attributes names that were not yet set for this event's entity. + * + * @retrun string[] + */ + public function getRemainingRequestedAttributes(): array + { + return $this->requestedAttributes; + } + + /** + * Sets a local data attribute of this event's entity and removes it from the list of requested attributes. + * + * @parem string $key The name of the attribute. + * + * @param mixed|null $value the value for the attribute + * + * @throws ApiError if attribute $key is not in the set of requested attributes + */ + public function setLocalDataAttribute(string $key, $value): void + { + $arrayKey = array_search($key, $this->requestedAttributes, true); + if ($arrayKey === false) { + // TODO: maybe ignore or just emit warning? + throw new ApiError(500, sprintf("trying to set local data attribute '%s', which was not requested for entity '%s'", $key, LocalDataAwareEventDispatcher::getUniqueEntityName(get_class($this->entity)))); + } + + // once set, remove the attribute from the list of requested attributes + array_splice($this->requestedAttributes, $arrayKey, 1); + $this->entity->setLocalDataValue($key, $value); + } + + /** + * Returns, whether a given attribute was requested for this event's entity. + * + * @parem string $key The name of the attribute. + */ + public function isLocalDataAttributeRequested(string $key): bool + { + return in_array($key, $this->requestedAttributes, true); + } +} diff --git a/src/Event/LocalDataAwarePostEvent.php b/src/Event/LocalDataAwarePostEvent.php deleted file mode 100644 index 5ba46740f6e17d53a5a385137330de8c53ff03b4..0000000000000000000000000000000000000000 --- a/src/Event/LocalDataAwarePostEvent.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Dbp\Relay\CoreBundle\Event; - -use Dbp\Relay\CoreBundle\Entity\LocalDataAwareInterface; -use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Contracts\EventDispatcher\Event; - -class LocalDataAwarePostEvent extends Event -{ - /** @var LocalDataAwareInterface */ - private $entity; - - /** @var array */ - private $requestedAttributes; - - protected function __construct(LocalDataAwareInterface $entity) - { - $this->entity = $entity; - } - - public function setRequestedAttributes(array $requestedAttributes) - { - $this->requestedAttributes = $requestedAttributes; - } - - public function getRemainingRequestedAttributes(): array - { - return $this->requestedAttributes; - } - - public function getEntity(): LocalDataAwareInterface - { - return $this->entity; - } - - public function setLocalDataAttribute(string $key, $value) - { - $arrayKey = array_search($key, $this->requestedAttributes, true); - if ($arrayKey === false) { - throw new HttpException(500, sprintf("local data attribute '%s' not requested for entity '%s'", $key, $this->entity->getUniqueEntityName())); - } - - // once set, remove the attribute from the list of requested attributes - array_splice($this->requestedAttributes, $arrayKey, 1); - $this->entity->setLocalDataValue($key, $value); - } - - public function isLocalDataAttributeRequested(string $key): bool - { - return in_array($key, $this->requestedAttributes, true); - } -} diff --git a/src/Service/LocalDataAwareEventDispatcher.php b/src/Service/LocalDataAwareEventDispatcher.php index 2d08fe2ed553b2217a2cd80d08de7ddc6c9644f2..c4077d5391dc9d3dbd39156bee977ee9d6d0a91a 100644 --- a/src/Service/LocalDataAwareEventDispatcher.php +++ b/src/Service/LocalDataAwareEventDispatcher.php @@ -4,16 +4,21 @@ declare(strict_types=1); namespace Dbp\Relay\CoreBundle\Service; -use Dbp\Relay\CoreBundle\Event\LocalDataAwarePostEvent; +use ApiPlatform\Core\Exception\ResourceClassNotFoundException; +use ApiPlatform\Core\Metadata\Resource\Factory\AnnotationResourceMetadataFactory; +use Dbp\Relay\CoreBundle\Event\LocalDataAwareEvent; +use Dbp\Relay\CoreBundle\Exception\ApiError; +use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Exception\HttpException; class LocalDataAwareEventDispatcher { + /** @var array */ private $requestedAttributes; /** @var string */ - private $unqiueEntityName; + private $uniqueEntityName; /** @var EventDispatcherInterface */ private $eventDispatcher; @@ -21,18 +26,23 @@ class LocalDataAwareEventDispatcher /** @var string */ private $eventName; - public function __construct(string $unqiueEntityName, EventDispatcherInterface $eventDispatcher, string $eventName) + public function __construct(string $resourceClass, EventDispatcherInterface $eventDispatcher, string $eventName) { - $this->unqiueEntityName = $unqiueEntityName; + $this->uniqueEntityName = self::getUniqueEntityName($resourceClass); $this->eventDispatcher = $eventDispatcher; $this->eventName = $eventName; } - public function initRequestedLocalDataAttributes(array $options) + /** + * Parses the 'include' option, if present, and extracts the list of requested attributes for $this->uniqueEntityName. + * + * @param string $includeParameter The value of the 'include' parameter as passed to a GET-operation + */ + public function initRequestedLocalDataAttributes(string $includeParameter): void { $this->requestedAttributes = []; - if ($include = $options['include'] ?? null) { - $requestedLocalDataAttributes = explode(',', $include); + if (!empty($includeParameter)) { + $requestedLocalDataAttributes = explode(',', $includeParameter); foreach ($requestedLocalDataAttributes as $requestedLocalDataAttribute) { $requestedLocalDataAttribute = trim($requestedLocalDataAttribute); @@ -43,7 +53,7 @@ class LocalDataAwareEventDispatcher throw new HttpException(400, sprintf("value of 'include' parameter has invalid format: '%s' (Example: 'ResourceName.attr,ResourceName.attr2')", $requestedLocalDataAttribute)); } - if ($this->unqiueEntityName === $requestedUniqueEntityName) { + if ($this->uniqueEntityName === $requestedUniqueEntityName) { $this->requestedAttributes[] = $requestedAttributeName; } } @@ -52,7 +62,10 @@ class LocalDataAwareEventDispatcher } } - public function dispatch(LocalDataAwarePostEvent $event) + /** + * Dispatches the given event. + */ + public function dispatch(LocalDataAwareEvent $event): void { $event->setRequestedAttributes($this->requestedAttributes); @@ -60,17 +73,46 @@ class LocalDataAwareEventDispatcher $remainingLocalDataAttributes = $event->getRemainingRequestedAttributes(); if (!empty($remainingLocalDataAttributes)) { - throw new HttpException(500, sprintf("the following local data attributes were not provided for resource '%s': %s", $this->unqiueEntityName, implode(', ', $remainingLocalDataAttributes))); + throw new HttpException(500, sprintf("the following local data attributes were not provided for resource '%s': %s", $this->uniqueEntityName, implode(', ', $remainingLocalDataAttributes))); } } - private static function parseLocalDataAttribute(string $localDataAttribute, ?string &$entityUniqueName, ?string &$attributeName): bool + /** + * Returns the unique API resource name, i.e. short name, of this entity. For this to work, the ApiResource annotation + * of the entity has to include a non-empty 'shortName' attribute. + * + * @throws ApiError if the ApiResource annotation of $resourceClass doesn't have a non-empty 'shortName' attribute + */ + public static function getUniqueEntityName(string $resourceClass): string + { + $resourceMetadataFactory = new AnnotationResourceMetadataFactory(new AnnotationReader()); + try { + $resourceMetadata = $resourceMetadataFactory->create($resourceClass); + } catch (ResourceClassNotFoundException $exc) { + throw new ApiError(500, $exc->getMessage()); + } + + $uniqueName = $resourceMetadata->getShortName() ?? ''; + if (empty($uniqueName)) { + throw new ApiError(500, sprintf("'shortName' attribute missing in ApiResource annotation of resource class '%s'", $resourceClass)); + } + + return $uniqueName; + } + + /** + * Parses a local data attribute of the form 'UniqueEntityName.attributeName'. + * NOTE: Due to possible performance impact, there is currently no regex check for valid entity and attribute names (i.e. PHP type/variable names). + * + * @retrun true if $localDataAttribute complies with the local attribute format, false otherwise + */ + private static function parseLocalDataAttribute(string $localDataAttribute, ?string &$uniqueEntityName, ?string &$attributeName): bool { $parts = explode('.', $localDataAttribute); if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) { return false; } - $entityUniqueName = $parts[0]; + $uniqueEntityName = $parts[0]; $attributeName = $parts[1]; return true;