Skip to content
Snippets Groups Projects
Select Git revision
  • f7d989aade6934e818cc17e714170753648eff2c
  • main default protected
  • v0.1.6
  • v0.1.5
  • v0.1.4
  • v0.1.3
  • v0.1.2
  • v0.1.1
  • v0.1.0
9 results

phpstan.neon

Blame
  • AbstractLocalDataEventSubscriber.php 9.03 KiB
    <?php
    
    declare(strict_types=1);
    
    namespace Dbp\Relay\CoreBundle\LocalData;
    
    use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService;
    use Dbp\Relay\CoreBundle\Exception\ApiError;
    use Symfony\Component\Config\Definition\Builder\NodeDefinition;
    use Symfony\Component\Config\Definition\Builder\TreeBuilder;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Contracts\EventDispatcher\Event;
    
    /*
     * Abstract implementation of a configurable local data provider post event subscriber.
     * It is intended to be derived by local data aware entity post event subscribers.
     * A mapping between source attribute and local data attribute,
     * and default values for the attributes can be specified by means of the deriving event subscriber's bundle config.
     * If no default value is specified, an exception is thrown in the case the mapped source attribute is not found.
     */
    abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface
    {
        protected const ROOT_CONFIG_NODE = 'local_data_mapping';
        protected const SOURCE_ATTRIBUTE_CONFIG_NODE = 'source_attribute';
        protected const LOCAL_DATA_ATTRIBUTE_CONFIG_NODE = 'local_data_attribute';
        protected const AUTHORIZATION_EXPRESSION_CONFIG_NODE = 'authorization_expression';
        protected const ALLOW_LOCAL_QUERY_CONFIG_NODE = 'allow_query';
        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 DEFAULT_VALUE_KEY = 'default';
        private const QUERYABLE_KEY = 'queryable';
    
        /*
         * WORKAROUND: could not find a way to determine whether a Symfony config array node was NOT specified since it provides an empty
         * array in case it is not specified. So I use an array value as default which does not seem to be reproducible by the configurator.
         */
        private const ARRAY_VALUE_NOT_SPECIFIED = [null => null];
    
        /** @var array */
        private $attributeMapping;
    
        public function __construct()
        {
            $this->attributeMapping = [];
        }
    
        public function setConfig(array $config)
        {
            $configNode = $config[self::ROOT_CONFIG_NODE] ?? [];
            $rightExpressions = [];
    
            foreach ($configNode as $configMappingEntry) {
                $localDataAttributeName = $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE];
    
                if (isset($this->attributeMapping[$localDataAttributeName])) {
                    throw new \RuntimeException(sprintf('multiple mapping entries for local data attribute %s', $localDataAttributeName));
                }
    
                $attributeMapEntry = [];
                $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTE_CONFIG_NODE];
                $attributeMapEntry[self::QUERYABLE_KEY] = $configMappingEntry[self::ALLOW_LOCAL_QUERY_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;
                    }
                }
    
                if ($defaultValue !== null) {
                    $attributeMapEntry[self::DEFAULT_VALUE_KEY] = $defaultValue;
                }
    
                $this->attributeMapping[$localDataAttributeName] = $attributeMapEntry;
    
                // the name of the local data attribute is used as name for the right to view that attribute
                // the attribute is visible false by default
                $rightExpressions[$localDataAttributeName] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_CONFIG_NODE] ?? 'false';
            }
    
            if (!empty($rightExpressions)) {
                parent::setConfig(parent::createConfig($rightExpressions));
            }
        }
    
        public static function getSubscribedEvents(): array
        {
            $eventMapping = [];
            foreach (static::getSubscribedEventNames() as $eventName) {
                $eventMapping[$eventName] = 'onEvent';
            }
    
            return $eventMapping;
        }
    
        public function onEvent(Event $event)
        {
            if ($event instanceof LocalDataPreEvent) {
                $localQueryParameters = [];
                foreach ($event->getPendingQueryParameters() as $localDataAttributeName => $localDataAttributeValue) {
                    if (($attributeMapEntry = $this->attributeMapping[$localDataAttributeName] ?? null) !== null &&
                        $attributeMapEntry[self::QUERYABLE_KEY] === true) {
                        if (!$this->isGranted($localDataAttributeName)) {
                            throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName));
                        }
                        $sourceAttributeName = $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY];
                        $localQueryParameters[$sourceAttributeName] = $localDataAttributeValue;
                        $event->acknowledgeQueryParameter($localDataAttributeName);
                    }
                }
    
                $this->onPreEvent($event, $localQueryParameters);
            } elseif ($event instanceof LocalDataPostEvent) {
                foreach ($event->getPendingRequestedAttributes() as $localDataAttributeName) {
                    if (($attributeMapEntry = $this->attributeMapping[$localDataAttributeName] ?? null) !== null) {
                        if (!$this->isGranted($localDataAttributeName)) {
                            throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName));
                        }
    
                        $attributeValue = $event->getSourceData()[$attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY]] ?? null;
                        $attributeValue = $attributeValue ?? $attributeMapEntry[self::DEFAULT_VALUE_KEY] ?? null;
    
                        if ($attributeValue !== null) {
                            $event->setLocalDataAttribute($localDataAttributeName, $attributeValue);
                        } else {
                            throw ApiError::withDetails(Response::HTTP_INTERNAL_SERVER_ERROR, sprintf('none of the source attributes available for local data attribute \'%s\'', $localDataAttributeName));
                        }
                    }
                }
                $this->onPostEvent($event);
            }
        }
    
        /**
         * @deprecated Use getLocalDataMappingConfigNodeDefinition instead
         */
        public static function getConfigNode(): NodeDefinition
        {
            return self::getLocalDataMappingConfigNodeDefinition();
        }
    
        public static function getLocalDataMappingConfigNodeDefinition(): NodeDefinition
        {
            $treeBuilder = new TreeBuilder(self::ROOT_CONFIG_NODE);
    
            return $treeBuilder->getRootNode()
                ->arrayPrototype()
                    ->children()
                        ->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE)
                            ->info('The name of the local data attribute.')
                        ->end()
                        ->scalarNode(self::SOURCE_ATTRIBUTE_CONFIG_NODE)
                            ->info('The source attribute to map to the local data attribute. If the source attribute is not found, the default value is used.')
                        ->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()
                        ->booleanNode(self::ALLOW_LOCAL_QUERY_CONFIG_NODE)
                            ->defaultValue('false')
                            ->info('Indicates whether the local data attribute can be used in local queries.')
                        ->end()
                        ->scalarNode(self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE)
                            ->info('The default value for scalar (i.e. non-array) attributes. If none is specified, an exception is thrown in case the source attribute is not 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 case the source attribute is not found.')
                            ->scalarPrototype()->end()
                        ->end()
                    ->end()
                ->end()
            ;
        }
    
        protected static function getSubscribedEventNames(): array
        {
            throw new \RuntimeException(sprintf('child classes must implement the \'%s\' method', __METHOD__));
        }
    
        protected function onPreEvent(LocalDataPreEvent $preEvent, array $localQueryParameters)
        {
        }
    
        protected function onPostEvent(LocalDataPostEvent $postEvent)
        {
        }
    }