diff --git a/.travis.yml b/.travis.yml index e71faeb..c718f0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,14 @@ language: php sudo: false php: - - "7.0" - "7.1" - "7.2" - "nightly" +env: + global: + - COMPOSER_MEMORY_LIMIT=-1 + matrix: allow_failures: - php: nightly @@ -30,7 +33,7 @@ before_install: install: - composer install - cd test/ - - composer install + - composer update - rm -rf vendor/pomm-project/api-platform - ln -s ../../../ vendor/pomm-project/api-platform diff --git a/src/DependencyInjection/Compiler/ResourceClassResolverPass.php b/src/DependencyInjection/Compiler/ResourceClassResolverPass.php new file mode 100644 index 0000000..5107e38 --- /dev/null +++ b/src/DependencyInjection/Compiler/ResourceClassResolverPass.php @@ -0,0 +1,32 @@ + + */ +final class ResourceClassResolverPass implements CompilerPassInterface +{ + /** + * {@inheritDoc} + */ + public function process(ContainerBuilder $container) + { + if ($container->hasDefinition('api_platform.resource_class_resolver')) { + $definition = new Definition('PommProject\ApiPlatform\ResourceClassResolver'); + $definition->addArgument(new Reference('api_platform.metadata.resource.name_collection_factory')); + + $container->setDefinition('api_platform.resource_class_resolver', $definition); + } + } +} diff --git a/src/ItemDataPersister.php b/src/ItemDataPersister.php new file mode 100644 index 0000000..b989d71 --- /dev/null +++ b/src/ItemDataPersister.php @@ -0,0 +1,102 @@ + + */ +final class ItemDataPersister implements ContextAwareDataPersisterInterface +{ + /** + * @var Pomm + */ + protected $pomm; + + public function __construct(Pomm $pomm) + { + $this->pomm = $pomm; + } + + /** + * {@inheritdoc} + */ + public function supports($data, array $context = []): bool + { + $proxyClass = new ReflectionClass($data); + + return $proxyClass->implementsInterface(FlexibleEntityInterface::class); + } + + /** + * {@inheritdoc} + */ + public function persist($data, array $context = []) + { + $model = $this->getModel($data, $context); + + if (null === $model) { + return $data; + } + + if (isset($context['collection_operation_name']) && 'post' === $context['collection_operation_name']) { + $model->insertOne($data); + } elseif (isset($context['item_operation_name']) && 'put' === $context['item_operation_name']) { + $fields = array_keys($data->fields()); + $model->updateOne($data, $fields); + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function remove($data, array $context = []) + { + $model = $this->getModel($data, $context); + + if (null === $model) { + return $data; + } + + if (isset($context['item_operation_name']) && 'delete' === $context['item_operation_name']) { + $model->deleteOne($data); + } + + return $data; + } + + protected function getModel($data, $context) + { + $proxyClass = new ReflectionClass($data); + $class = $proxyClass->getName(); + + if (isset($context['session:name'])) { + $session = $this->pomm->getSession($context['session:name']); + } else { + $session = $this->pomm->getDefaultSession(); + } + + if (isset($context['model:name'])) { + $model_name = $context['model:name']; + } else { + $model_name = "${class}Model"; + } + + if (!class_exists($model_name)) { + return; + } + + return $session->getModel($model_name); + } +} diff --git a/src/ItemDataProvider.php b/src/ItemDataProvider.php index d434004..1f83502 100644 --- a/src/ItemDataProvider.php +++ b/src/ItemDataProvider.php @@ -12,10 +12,13 @@ namespace PommProject\ApiPlatform; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; +use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; use PommProject\Foundation\Pomm; +use PommProject\ModelManager\Model\FlexibleEntity\FlexibleEntityInterface; +use ReflectionClass; -class ItemDataProvider implements ItemDataProviderInterface +class ItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { private $pomm; @@ -24,6 +27,13 @@ public function __construct(Pomm $pomm) $this->pomm = $pomm; } + public function supports(string $resourceClass, string $operationName = null, array $context = []): bool + { + $proxyClass = new ReflectionClass($resourceClass); + + return $proxyClass->implementsInterface(FlexibleEntityInterface::class); + } + /** * {@inheritdoc} */ diff --git a/src/Metadata/Property/PommPropertyMetadataFactory.php b/src/Metadata/Property/PommPropertyMetadataFactory.php new file mode 100644 index 0000000..2640aaa --- /dev/null +++ b/src/Metadata/Property/PommPropertyMetadataFactory.php @@ -0,0 +1,73 @@ + + */ +final class PommPropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + private $pomm; + + private $decorated; + + public function __construct(Pomm $pomm, PropertyMetadataFactoryInterface $decorated) + { + $this->pomm = $pomm; + $this->decorated = $decorated; + } + + /** + * Creates a property metadata. + * + * @throws PropertyNotFoundException + */ + public function create(string $resourceClass, string $property, array $options = []): PropertyMetadata + { + $propertyMetadata = $this->decorated->create($resourceClass, $property, $options); + + if (null !== $propertyMetadata->isIdentifier()) { + return $propertyMetadata; + } + + $session = $this->pomm->getDefaultSession(); + $modelName = "${resourceClass}Model"; + + if (!class_exists($modelName)) { + return $propertyMetadata; + } + + $model = $session->getModel($modelName); + $fieldNames = $model->getStructure() + ->getFieldNames(); + + $primaryKeys = $model->getStructure() + ->getPrimaryKey(); + + if (in_array($property, $primaryKeys)) { + $propertyMetadata = $propertyMetadata->withIdentifier(true); + } + + if (null === $propertyMetadata->isIdentifier()) { + $propertyMetadata = $propertyMetadata->withIdentifier(false); + } + + if (in_array($property, $fieldNames)) { + $propertyMetadata = $propertyMetadata->withReadable(true); + $propertyMetadata = $propertyMetadata->withWritable(true); + } + + return $propertyMetadata; + } +} diff --git a/src/PommApiPlatformBundle.php b/src/PommApiPlatformBundle.php index 6c5b96d..292bc69 100644 --- a/src/PommApiPlatformBundle.php +++ b/src/PommApiPlatformBundle.php @@ -3,8 +3,16 @@ namespace PommProject\ApiPlatform; +use PommProject\ApiPlatform\DependencyInjection\Compiler\ResourceClassResolverPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; use \Symfony\Component\HttpKernel\Bundle\Bundle; class PommApiPlatformBundle extends Bundle { + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new ResourceClassResolverPass()); + } } diff --git a/src/ResourceClassResolver.php b/src/ResourceClassResolver.php new file mode 100644 index 0000000..17a8c28 --- /dev/null +++ b/src/ResourceClassResolver.php @@ -0,0 +1,95 @@ + + */ +final class ResourceClassResolver implements ResourceClassResolverInterface +{ + use ClassInfoTrait; + + private $resourceNameCollectionFactory; + private $localIsResourceClassCache = []; + + public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory) + { + $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; + } + + /** + * {@inheritdoc} + */ + public function getResourceClass($value, string $resourceClass = null, bool $strict = false): string + { + if ($strict && null === $resourceClass) { + throw new InvalidArgumentException('Strict checking is only possible when resource class is specified.'); + } + + $actualClass = \is_object($value) && (!$value instanceof \Traversable || $value instanceof FlexibleEntityInterface) ? $this->getObjectClass($value) : null; + + if (null === $actualClass && null === $resourceClass) { + throw new InvalidArgumentException('Resource type could not be determined. Resource class must be specified.'); + } + + if (null !== $actualClass && !$this->isResourceClass($actualClass)) { + throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s".', $actualClass)); + } + + if (null !== $resourceClass && !$this->isResourceClass($resourceClass)) { + throw new InvalidArgumentException(sprintf('Specified class "%s" is not a resource class.', $resourceClass)); + } + + if ($strict && null !== $actualClass && !is_a($actualClass, $resourceClass, true)) { + throw new InvalidArgumentException(sprintf('Object of type "%s" does not match "%s" resource class.', $actualClass, $resourceClass)); + } + + $targetClass = $actualClass ?? $resourceClass; + $mostSpecificResourceClass = null; + + foreach ($this->resourceNameCollectionFactory->create() as $resourceClassName) { + if (!is_a($targetClass, $resourceClassName, true)) { + continue; + } + + if (null === $mostSpecificResourceClass || is_subclass_of($resourceClassName, $mostSpecificResourceClass)) { + $mostSpecificResourceClass = $resourceClassName; + } + } + + if (null === $mostSpecificResourceClass) { + throw new \LogicException('Unexpected execution flow.'); + } + + return $mostSpecificResourceClass; + } + + /** + * {@inheritdoc} + */ + public function isResourceClass(string $type): bool + { + if (isset($this->localIsResourceClassCache[$type])) { + return $this->localIsResourceClassCache[$type]; + } + + foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { + if (is_a($type, $resourceClass, true)) { + return $this->localIsResourceClassCache[$type] = true; + } + } + + return $this->localIsResourceClassCache[$type] = false; + } +} + diff --git a/src/Resources/config/pomm.yml b/src/Resources/config/pomm.yml index a3f1291..d0d0064 100644 --- a/src/Resources/config/pomm.yml +++ b/src/Resources/config/pomm.yml @@ -31,11 +31,24 @@ services: tags: - { name: 'api_platform.item_data_provider' } - api_platform.pomm.listener.view.write: - class: 'PommProject\ApiPlatform\WriteListener' + api_platform.pomm.item_data_persister: + class: 'PommProject\ApiPlatform\ItemDataPersister' arguments: ['@pomm'] tags: - - { name: 'kernel.event_listener', event: 'kernel.view', method: 'onKernelView', priority: 32 } + - { name: 'api_platform.data_persister' } + + api_platform.pomm.property_metadata: + class: 'PommProject\ApiPlatform\Metadata\Property\PommPropertyMetadataFactory' + arguments: ['@pomm', '@api_platform.pomm.property_metadata.inner'] + decorates: 'api_platform.metadata.property.metadata_factory' + decoration_priority: 39 + public: false + + #api_platform.pomm.listener.view.write: + # class: 'PommProject\ApiPlatform\WriteListener' + #arguments: ['@pomm'] + #tags: + #- { name: 'kernel.event_listener', event: 'kernel.view', method: 'onKernelView', priority: 32 } api_platform.pomm.search_filter: public: false diff --git a/test/app/AppKernel.php b/test/app/AppKernel.php index 49aae3e..057a3e0 100644 --- a/test/app/AppKernel.php +++ b/test/app/AppKernel.php @@ -22,7 +22,6 @@ public function registerBundles(): array { $bundles = [ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new \Symfony\Bundle\AsseticBundle\AsseticBundle(), new \Symfony\Bundle\MonologBundle\MonologBundle(), new \Symfony\Bundle\TwigBundle\TwigBundle(), new \PommProject\PommBundle\PommBundle(), diff --git a/test/app/config/config.yml b/test/app/config/config.yml index b6bbfac..da13202 100644 --- a/test/app/config/config.yml +++ b/test/app/config/config.yml @@ -20,7 +20,6 @@ framework: #assets_version: SomeVersionScheme default_locale: "%locale%" trusted_hosts: ~ - trusted_proxies: ~ session: # handler_id set to null will use default session handler from php.ini handler_id: ~ @@ -33,19 +32,6 @@ twig: debug: "%kernel.debug%" strict_variables: "%kernel.debug%" -# Assetic Configuration -assetic: - debug: "%kernel.debug%" - use_controller: "%kernel.debug%" - bundles: [ 'AppBundle' ] - #java: /usr/bin/java - filters: - cssrewrite: ~ - #closure: - # jar: "%kernel.root_dir%/Resources/java/compiler.jar" - #yui_css: - # jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar" - # Logging configuration monolog: handlers: @@ -63,6 +49,9 @@ pomm: api_platform: title: 'Pomm test' + mapping: + paths: + - '%kernel.project_dir%/src/Entity' collection: pagination: page_parameter_name: myPage diff --git a/test/behat.yml.dist b/test/behat.yml.dist index 5a399f4..8ef2516 100644 --- a/test/behat.yml.dist +++ b/test/behat.yml.dist @@ -1,7 +1,8 @@ default: suites: default: - paths: [ %paths.base%/tests/Features ] + paths: + - '%paths.base%/tests/Features' contexts: - Behat\MinkExtension\Context\MinkContext - behatch:context:json diff --git a/test/composer.json b/test/composer.json index 64e9f39..ce6e536 100644 --- a/test/composer.json +++ b/test/composer.json @@ -2,14 +2,15 @@ "require": { "php": ">=7.0", "api-platform/core": "~2.0", - "symfony/symfony": "~3.0", - "symfony/assetic-bundle": "~2.7", - "symfony/monolog-bundle": "~3.0", + "symfony/symfony": "~4.0", + "symfony/asset": "^3.4||^4.0", + "symfony/monolog-bundle": "~3.4||~4.0", "pomm-project/pomm-bundle": "~2.4", "pomm-project/api-platform": "dev-master" }, "require-dev": { - "behatch/contexts": "~2.6", + "behat/mink": "dev-master", + "behatch/contexts": "^3.2", "behat/mink-goutte-driver": "*" }, "autoload": { diff --git a/test/src/Entity/Config.php b/test/src/Entity/Config.php index 75f73be..8449175 100644 --- a/test/src/Entity/Config.php +++ b/test/src/Entity/Config.php @@ -2,6 +2,7 @@ namespace AppBundle\Entity; +use ApiPlatform\Core\Annotation\ApiResource; use PommProject\ModelManager\Model\FlexibleEntity; /** @@ -11,6 +12,9 @@ * public.config * * @see FlexibleEntity + * + * @ApiResource + * */ class Config extends FlexibleEntity {