diff --git a/src/LocalData/AbstractLocalDataPostEventSubscriber.php b/src/LocalData/AbstractLocalDataPostEventSubscriber.php index 0879e34f4a70f04dda2aab13732fadf692f46f98..1ab264497d3e507735b6d984acbfeb51c786a476 100644 --- a/src/LocalData/AbstractLocalDataPostEventSubscriber.php +++ b/src/LocalData/AbstractLocalDataPostEventSubscriber.php @@ -21,13 +21,13 @@ use Symfony\Component\HttpFoundation\Response; abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface { protected const ROOT_CONFIG_NODE = 'local_data_mapping'; - protected const SOURCE_ATTRIBUTE_CONFIG_NODE = 'source_attribute'; + protected const SOURCE_ATTRIBUTES_CONFIG_NODE = 'source_attributes'; protected const LOCAL_DATA_ATTRIBUTE_CONFIG_NODE = 'local_data_attribute'; protected const AUTHORIZATION_EXPRESSION_CONFIG_NODE = 'authorization_expression'; protected const DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE = 'default_value'; protected const DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE = 'default_values'; - private const SOURCE_ATTRIBUTE_KEY = 'source'; + private const SOURCE_ATTRIBUTES_KEY = 'source'; private const DEFAULT_VALUE_KEY = 'default'; /* @@ -57,10 +57,16 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio } $attributeMapEntry = []; - $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTE_CONFIG_NODE]; + $attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTES_CONFIG_NODE]; + + $defaultValue = $configMappingEntry[self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE] ?? null; + if ($defaultValue === null) { + $defaultArray = $configMappingEntry[self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE] ?? null; + if ($defaultArray !== null && $defaultArray !== self::ARRAY_VALUE_NOT_SPECIFIED) { + $defaultValue = $defaultArray; + } + } - $defaultValue = $configMappingEntry[self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE] ?? - ((($defaultArray = $configMappingEntry[self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE]) !== self::ARRAY_VALUE_NOT_SPECIFIED) ? $defaultArray : null); if ($defaultValue !== null) { $attributeMapEntry[self::DEFAULT_VALUE_KEY] = $defaultValue; } @@ -97,12 +103,19 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName)); } - $sourceAttributeName = $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY]; - $attributeValue = $sourceData[$sourceAttributeName] ?? $attributeMapEntry[self::DEFAULT_VALUE_KEY] ?? null; + $attributeValue = null; + foreach ($attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY] as $sourceAttributeName) { + if (($value = $sourceData[$sourceAttributeName] ?? null) !== null) { + $attributeValue = $value; + break; + } + } + + $attributeValue = $attributeValue ?? $attributeMapEntry[self::DEFAULT_VALUE_KEY] ?? null; if ($attributeValue !== null) { $postEvent->setLocalDataAttribute($localDataAttributeName, $attributeValue); } else { - throw ApiError::withDetails(Response::HTTP_INTERNAL_SERVER_ERROR, sprintf('attribute \'%s\' not available in source data', $sourceAttributeName)); + throw ApiError::withDetails(Response::HTTP_INTERNAL_SERVER_ERROR, sprintf('none of the source attributes available for local data attribute \'%s\'', $localDataAttributeName)); } } } @@ -123,17 +136,23 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio return $treeBuilder->getRootNode() ->arrayPrototype() ->children() - ->scalarNode(self::SOURCE_ATTRIBUTE_CONFIG_NODE)->end() - ->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE)->end() + ->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE) + ->info('The name of the local data attribute.') + ->end() + ->arrayNode(self::SOURCE_ATTRIBUTES_CONFIG_NODE) + ->info('The list of source attributes to map to the local data attribute ordered by preferred usage. If an attribute is not found, the next attribute in the list is used.') + ->scalarPrototype()->end() + ->end() ->scalarNode(self::AUTHORIZATION_EXPRESSION_CONFIG_NODE) ->defaultValue('false') + ->info('A boolean expression evaluable by the Symfony Expression Language determining whether the current user may request read the local data attribute.') ->end() ->scalarNode(self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE) - ->info('The default value for scalar (non-array) attributes. If none is specified, an exception is thrown in the case the source attribute is not found.') + ->info('The default value for scalar (i.e. non-array) attributes. If none is specified, an exception is thrown in case none of the source attributes is found.') ->end() ->arrayNode(self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE) ->defaultValue(self::ARRAY_VALUE_NOT_SPECIFIED) - ->info('The default value for array type attributes. If none is specified, an exception is thrown in the case the source attribute is not found.') + ->info('The default value for array type attributes. If none is specified, an exception is thrown in case none of the source attributes is found.') ->scalarPrototype()->end() ->end() ->end() diff --git a/src/LocalData/LocalDataEventDispatcher.php b/src/LocalData/LocalDataEventDispatcher.php index fb174d30d6b7afa4bc0595413c29f0c9ba65b7dc..0e60a3dc06d5dd4ad80eab1c85e9b7335383d78f 100644 --- a/src/LocalData/LocalDataEventDispatcher.php +++ b/src/LocalData/LocalDataEventDispatcher.php @@ -30,14 +30,15 @@ class LocalDataEventDispatcher private $eventDispatcher; /** - * @param string $resourceClass The class name of the entity (resource) this event dispatcher is responsible for - * @param EventDispatcherInterface $eventDispatcher The inner event dispatcher that this event dispatcher decorates + * @param string $resourceClass The class name of the entity (resource) this event dispatcher is responsible for + * @param EventDispatcherInterface $eventDispatcher The inner event dispatcher that this event dispatcher decorates + * @param string|null $uniqueEntityName The unique name of the entity. If not specified or empty, the 'shortName' attribute of the entities @ApiResource annotation is used. */ - public function __construct(string $resourceClass, EventDispatcherInterface $eventDispatcher) + public function __construct(string $resourceClass, EventDispatcherInterface $eventDispatcher, string $uniqueEntityName = null) { $this->queryParameters = []; $this->requestedAttributes = []; - $this->uniqueEntityName = self::getUniqueEntityName($resourceClass); + $this->uniqueEntityName = !Tools::isNullOrEmpty($uniqueEntityName) ? $uniqueEntityName : self::getUniqueEntityName($resourceClass); $this->eventDispatcher = $eventDispatcher; } @@ -113,7 +114,7 @@ class LocalDataEventDispatcher } $uniqueName = $resourceMetadata->getShortName() ?? ''; - if (empty($uniqueName)) { + if (Tools::isNullOrEmpty($uniqueName)) { throw new ApiError(500, sprintf("'shortName' attribute missing in ApiResource annotation of resource class '%s'", $resourceClass)); } elseif (str_contains($uniqueName, '.') || str_contains($uniqueName, ',')) { throw new ApiError(500, sprintf("'shortName' attribute of resource class '%s' must not contain '.' or ',' characters: '%s'", $resourceClass, $uniqueName));