Skip to content
Snippets Groups Projects
Commit 5b8caf2c authored by Tobias Gross-Vogt's avatar Tobias Gross-Vogt
Browse files

localData mechanism docu

parent 6eb12a5a
Branches
No related tags found
No related merge requests found
Pipeline #99738 passed
#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
...@@ -6,22 +6,20 @@ namespace Dbp\Relay\CoreBundle\Entity; ...@@ -6,22 +6,20 @@ namespace Dbp\Relay\CoreBundle\Entity;
interface LocalDataAwareInterface 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. * 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; 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); public function getLocalDataValue(string $key);
} }
...@@ -18,15 +18,19 @@ trait LocalDataAwareTrait ...@@ -18,15 +18,19 @@ trait LocalDataAwareTrait
*/ */
private $localData; private $localData;
/**
* Returns the array of local data attributes.
*/
public function getLocalData(): array public function getLocalData(): array
{ {
return $this->localData; 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 public function setLocalDataValue(string $key, $value): void
{ {
...@@ -38,9 +42,11 @@ trait LocalDataAwareTrait ...@@ -38,9 +42,11 @@ trait LocalDataAwareTrait
/** /**
* @Ignore * @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) public function getLocalDataValue(string $key)
{ {
......
...@@ -5,10 +5,11 @@ declare(strict_types=1); ...@@ -5,10 +5,11 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Event; namespace Dbp\Relay\CoreBundle\Event;
use Dbp\Relay\CoreBundle\Entity\LocalDataAwareInterface; use Dbp\Relay\CoreBundle\Entity\LocalDataAwareInterface;
use Symfony\Component\HttpKernel\Exception\HttpException; use Dbp\Relay\CoreBundle\Exception\ApiError;
use Dbp\Relay\CoreBundle\Service\LocalDataAwareEventDispatcher;
use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\EventDispatcher\Event;
class LocalDataAwarePostEvent extends Event class LocalDataAwareEvent extends Event
{ {
/** @var LocalDataAwareInterface */ /** @var LocalDataAwareInterface */
private $entity; private $entity;
...@@ -21,26 +22,41 @@ class LocalDataAwarePostEvent extends Event ...@@ -21,26 +22,41 @@ class LocalDataAwarePostEvent extends Event
$this->entity = $entity; $this->entity = $entity;
} }
public function setRequestedAttributes(array $requestedAttributes) /**
* 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; $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 public function getRemainingRequestedAttributes(): array
{ {
return $this->requestedAttributes; return $this->requestedAttributes;
} }
public function getEntity(): LocalDataAwareInterface /**
{ * Sets a local data attribute of this event's entity and removes it from the list of requested attributes.
return $this->entity; *
} * @parem string $key The name of the attribute.
*
public function setLocalDataAttribute(string $key, $value) * @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); $arrayKey = array_search($key, $this->requestedAttributes, true);
if ($arrayKey === false) { if ($arrayKey === false) {
throw new HttpException(500, sprintf("local data attribute '%s' not requested for entity '%s'", $key, $this->entity->getUniqueEntityName())); // 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 // once set, remove the attribute from the list of requested attributes
...@@ -48,6 +64,11 @@ class LocalDataAwarePostEvent extends Event ...@@ -48,6 +64,11 @@ class LocalDataAwarePostEvent extends Event
$this->entity->setLocalDataValue($key, $value); $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 public function isLocalDataAttributeRequested(string $key): bool
{ {
return in_array($key, $this->requestedAttributes, true); return in_array($key, $this->requestedAttributes, true);
......
...@@ -4,16 +4,21 @@ declare(strict_types=1); ...@@ -4,16 +4,21 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Service; 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\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
class LocalDataAwareEventDispatcher class LocalDataAwareEventDispatcher
{ {
/** @var array */
private $requestedAttributes; private $requestedAttributes;
/** @var string */ /** @var string */
private $unqiueEntityName; private $uniqueEntityName;
/** @var EventDispatcherInterface */ /** @var EventDispatcherInterface */
private $eventDispatcher; private $eventDispatcher;
...@@ -21,18 +26,23 @@ class LocalDataAwareEventDispatcher ...@@ -21,18 +26,23 @@ class LocalDataAwareEventDispatcher
/** @var string */ /** @var string */
private $eventName; 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->eventDispatcher = $eventDispatcher;
$this->eventName = $eventName; $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 = []; $this->requestedAttributes = [];
if ($include = $options['include'] ?? null) { if (!empty($includeParameter)) {
$requestedLocalDataAttributes = explode(',', $include); $requestedLocalDataAttributes = explode(',', $includeParameter);
foreach ($requestedLocalDataAttributes as $requestedLocalDataAttribute) { foreach ($requestedLocalDataAttributes as $requestedLocalDataAttribute) {
$requestedLocalDataAttribute = trim($requestedLocalDataAttribute); $requestedLocalDataAttribute = trim($requestedLocalDataAttribute);
...@@ -43,7 +53,7 @@ class LocalDataAwareEventDispatcher ...@@ -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)); 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; $this->requestedAttributes[] = $requestedAttributeName;
} }
} }
...@@ -52,7 +62,10 @@ class LocalDataAwareEventDispatcher ...@@ -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); $event->setRequestedAttributes($this->requestedAttributes);
...@@ -60,17 +73,46 @@ class LocalDataAwareEventDispatcher ...@@ -60,17 +73,46 @@ class LocalDataAwareEventDispatcher
$remainingLocalDataAttributes = $event->getRemainingRequestedAttributes(); $remainingLocalDataAttributes = $event->getRemainingRequestedAttributes();
if (!empty($remainingLocalDataAttributes)) { 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); $parts = explode('.', $localDataAttribute);
if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) { if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
return false; return false;
} }
$entityUniqueName = $parts[0]; $uniqueEntityName = $parts[0];
$attributeName = $parts[1]; $attributeName = $parts[1];
return true; return true;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment