From cf71562a9874a5b7e0e1dc8e9a2ccfc3dfe0f171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Karlovi=C4=87?= Date: Fri, 25 Feb 2022 11:29:05 +0100 Subject: [PATCH] feat: fetch by ID (#89) --- config/services.yaml | 18 +++++- .../ConfigureDatabasesCompilerPass.php | 14 ++--- .../ExpressionLanguage/FunctionProvider.php | 8 +++ .../Normalizer/ExpressionNormalizer.php | 53 ++++++++++++++++ .../Twig/Extension/DatabaseExtension.php | 3 + src/Database.php | 26 ++++++++ src/Database/MemoryDatabase.php | 11 +++- src/DatabaseProvider.php | 25 ++++---- src/Storage.php | 13 +++- src/Storage/DenormalizingStorage.php | 60 +++++++++++++++---- src/Storage/FilesystemStorage.php | 38 ++++++++---- src/Storage/MemoryStorage.php | 16 ++++- src/StorageWithOptions.php | 23 +++++++ .../site/config/packages/yassg_databases.yaml | 6 +- .../site/content/categories/category1.yaml | 4 ++ .../site/content/products/test2.en.yaml | 8 +-- tests/functional/site/src/Model/Category.php | 5 ++ .../site/templates/pages/homepage.html.twig | 5 ++ .../site/templates/pages/product.html.twig | 9 ++- 19 files changed, 279 insertions(+), 66 deletions(-) create mode 100644 src/Bridge/Symfony/Serializer/Normalizer/ExpressionNormalizer.php create mode 100644 src/StorageWithOptions.php diff --git a/config/services.yaml b/config/services.yaml index 95a081d..a47801c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -12,6 +12,7 @@ services: exclude: - '../src/Bridge/Symfony/Serializer/*' - '../src/Database/*' + - '../src/DatabaseProvider.php' - '../src/Kernel.php' Sigwin\YASSG\Bridge\Symfony\Controller\: @@ -32,6 +33,10 @@ services: - name: sigwin_yassg.database.storage.type type: memory + + Sigwin\YASSG\DatabaseProvider: + arguments: + $locator: !tagged_locator { tag: 'sigwin_yassg.database', index_by: 'name' } sigwin_yassg.expression_language: class: Symfony\Component\ExpressionLanguage\ExpressionLanguage @@ -47,8 +52,7 @@ services: tags: - name: serializer.normalizer - priority: 3000 - + priority: 3000 sigwin_yassg.serializer.denormalizer.collection: class: Sigwin\YASSG\Bridge\Symfony\Serializer\Normalizer\CollectionNormalizer arguments: @@ -56,7 +60,15 @@ services: tags: - name: serializer.normalizer - priority: 5000 + priority: 5000 + sigwin_yassg.serializer.denormalizer.expression: + class: Sigwin\YASSG\Bridge\Symfony\Serializer\Normalizer\ExpressionNormalizer + arguments: + $expressionLanguage: '@sigwin_yassg.expression_language' + tags: + - + name: serializer.normalizer + priority: 4000 sigwin_yassg.fragment.renderer.inline: class: Sigwin\YASSG\Bridge\Symfony\HttpKernel\Fragment\RelativeUrlInlineFragmentRenderer diff --git a/src/Bridge/Symfony/DependencyInjection/CompilerPass/ConfigureDatabasesCompilerPass.php b/src/Bridge/Symfony/DependencyInjection/CompilerPass/ConfigureDatabasesCompilerPass.php index 9ed879e..0bd3490 100644 --- a/src/Bridge/Symfony/DependencyInjection/CompilerPass/ConfigureDatabasesCompilerPass.php +++ b/src/Bridge/Symfony/DependencyInjection/CompilerPass/ConfigureDatabasesCompilerPass.php @@ -17,8 +17,8 @@ use Sigwin\YASSG\Bridge\Symfony\Serializer\AttributeMetadataTrait; use Sigwin\YASSG\Database; use Sigwin\YASSG\Database\MemoryDatabase; -use Sigwin\YASSG\DatabaseProvider; use Sigwin\YASSG\Storage; +use Sigwin\YASSG\StorageWithOptions; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -48,7 +48,7 @@ public function process(ContainerBuilder $container): void throw new \LogicException(sprintf('Data source type %1$s already provided by %2$s', $type, $reference)); } - /** @var class-string $class */ + /** @var class-string $class */ $class = $storageDefinition->getClass(); $supportedStorageTypes[$type] = $class; @@ -56,7 +56,6 @@ public function process(ContainerBuilder $container): void } unset($type); - $databases = []; $localizableClasses = []; $classMetadata = $container->get('serializer.mapping.class_metadata_factory'); @@ -80,6 +79,7 @@ public function process(ContainerBuilder $container): void $storageDefinition ->setAutowired(true) ->setAutoconfigured(true); + $callable = [$supportedStorageTypes[$type], 'resolveOptions']; try { $options = $callable($database['options'] ?? []); @@ -107,22 +107,18 @@ public function process(ContainerBuilder $container): void $databaseDefinition ->setArgument(0, new Reference($storageId)) ->setArgument(1, new Reference('sigwin_yassg.expression_language')) - ->setArgument(2, $this->getProperties($databaseClass)); + ->setArgument(2, $this->getProperties($databaseClass)) + ->addTag('sigwin_yassg.database', ['name' => $name]); $databaseId = sprintf('sigwin_yassg.database.%1$s', $name); $container->setDefinition($databaseId, $databaseDefinition); $container->setAlias(sprintf('%1$s $%2$s', Database::class, $name), $databaseId); - $databases[$name] = new Reference($databaseId); - $localizableProperties = $this->getLocalizableProperties($databaseClass); if ($localizableProperties !== []) { $localizableClasses[$databaseClass] = $localizableProperties; } } $container->setParameter('sigwin_yassg.databases_spec', null); - $container - ->getDefinition(DatabaseProvider::class) - ->setArgument(0, $databases); $container ->getDefinition('sigwin_yassg.serializer.denormalizer.localizing') ->setArgument(0, $localizableClasses); diff --git a/src/Bridge/Symfony/ExpressionLanguage/FunctionProvider.php b/src/Bridge/Symfony/ExpressionLanguage/FunctionProvider.php index e5f4236..889ba30 100644 --- a/src/Bridge/Symfony/ExpressionLanguage/FunctionProvider.php +++ b/src/Bridge/Symfony/ExpressionLanguage/FunctionProvider.php @@ -30,6 +30,14 @@ public function getFunctions(): array return $provider->getDatabase($name)->findAll(...$arguments); }), + new ExpressionFunction('yassg_get', static function (string $name): string { + return sprintf('$provider->getDatabase(%s)', $name); + }, static function (array $variables, string $name, string $id) { + /** @var DatabaseProvider $provider */ + $provider = $variables['provider']; + + return $provider->getDatabase($name)->get($id); + }), ]; } } diff --git a/src/Bridge/Symfony/Serializer/Normalizer/ExpressionNormalizer.php b/src/Bridge/Symfony/Serializer/Normalizer/ExpressionNormalizer.php new file mode 100644 index 0000000..cb16b71 --- /dev/null +++ b/src/Bridge/Symfony/Serializer/Normalizer/ExpressionNormalizer.php @@ -0,0 +1,53 @@ +expressionLanguage = $expressionLanguage; + $this->databaseProvider = $databaseProvider; + } + + public function denormalize(mixed $data, string $type, string $format = null, array $context = []) + { + /** @var string $data */ + $value = $this->expressionLanguage->evaluate(mb_substr($data, 2), [ + 'provider' => $this->databaseProvider, + ]); + + /** + * @var class-string $type + * @phpstan-ignore-next-line + */ + if (is_a($value, $type, false) === false) { + throw new \LogicException(sprintf('Invalid value denormalized, %1$s expected, %2$s received', $type, get_debug_type($value))); + } + + return $value; + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null): bool + { + return \is_string($data) && str_starts_with($data, '@='); + } +} diff --git a/src/Bridge/Twig/Extension/DatabaseExtension.php b/src/Bridge/Twig/Extension/DatabaseExtension.php index 76cec99..0dddcd5 100644 --- a/src/Bridge/Twig/Extension/DatabaseExtension.php +++ b/src/Bridge/Twig/Extension/DatabaseExtension.php @@ -47,6 +47,9 @@ public function getFunctions(): array new TwigFunction('yassg_find_one_by_or_null', function (string $name, array $arguments = []) { return $this->provider->getDatabase($name)->findOneByOrNull(...$arguments); }), + new TwigFunction('yassg_get', function (string $name, string $id) { + return $this->provider->getDatabase($name)->get($id); + }), ]; } } diff --git a/src/Database.php b/src/Database.php index 60d8844..93f69e5 100644 --- a/src/Database.php +++ b/src/Database.php @@ -13,21 +13,47 @@ namespace Sigwin\YASSG; +/** + * @template T of object + */ interface Database { public function count(?string $condition = null): int; public function countBy(array $condition): int; + /** + * @return Collection + */ public function findAll(?string $condition = null, ?array $sort = null, ?int $limit = null, int $offset = 0, ?string $select = null): Collection; + /** + * @return Collection + */ public function findAllBy(array $condition, ?array $sort = null, ?int $limit = null, int $offset = 0, ?string $select = null): Collection; + /** + * @return T + */ public function findOne(?string $condition = null, ?array $sort = null, ?string $select = null): object; + /** + * @return T + */ public function findOneBy(array $condition, ?array $sort = null, ?string $select = null): object; + /** + * @return null|T + */ public function findOneOrNull(?string $condition = null, ?array $sort = null, ?string $select = null): ?object; + /** + * @return null|T + */ public function findOneByOrNull(array $condition, ?array $sort = null, ?string $select = null): ?object; + + /** + * @return T + */ + public function get(string $id): object; } diff --git a/src/Database/MemoryDatabase.php b/src/Database/MemoryDatabase.php index d3aa975..51d67d6 100644 --- a/src/Database/MemoryDatabase.php +++ b/src/Database/MemoryDatabase.php @@ -121,6 +121,14 @@ public function findOneByOrNull(array $condition, ?array $sort = null, ?string $ return $this->findOneOrNull($this->conditionArrayToString($condition), $sort, $select); } + public function get(string $id): object + { + /** @var object $item */ + $item = $this->storage->get($id); + + return $item; + } + private function load(?string $condition, callable $callable): void { $conditionExpression = null; @@ -129,9 +137,6 @@ private function load(?string $condition, callable $callable): void } foreach ($this->storage->load() as $id => $item) { - if ($item === null) { - continue; - } if ($conditionExpression === null || $this->expressionLanguage->evaluate($conditionExpression, ['item' => $item]) !== false) { $callable($id, $item); } diff --git a/src/DatabaseProvider.php b/src/DatabaseProvider.php index 53070af..25e525c 100644 --- a/src/DatabaseProvider.php +++ b/src/DatabaseProvider.php @@ -13,27 +13,28 @@ namespace Sigwin\YASSG; +use Symfony\Component\DependencyInjection\ServiceLocator; + final class DatabaseProvider { - /** - * @var array - */ - private array $databases; - - /** - * @param array $databases - */ - public function __construct(array $databases) + private ServiceLocator $locator; + + public function __construct(ServiceLocator $locator) { - $this->databases = $databases; + $this->locator = $locator; } public function getDatabase(string $name): Database { - if (isset($this->databases[$name]) === false) { + if ($this->locator->has($name) === false) { throw new \LogicException(sprintf('No such database "%1$s"', $name)); } - return $this->databases[$name]; + $database = $this->locator->get($name); + if ($database instanceof Database === false) { + throw new \LogicException(sprintf('Service "%1$s" is not a database', $name)); + } + + return $database; } } diff --git a/src/Storage.php b/src/Storage.php index c0f2d5a..bd57581 100644 --- a/src/Storage.php +++ b/src/Storage.php @@ -13,9 +13,18 @@ namespace Sigwin\YASSG; +/** + * @template T of object + */ interface Storage { - public static function resolveOptions(array $options): array; - + /** + * @return iterable + */ public function load(): iterable; + + /** + * @return array|T + */ + public function get(string $id): object|array; } diff --git a/src/Storage/DenormalizingStorage.php b/src/Storage/DenormalizingStorage.php index 4fd1dc5..d0c9289 100644 --- a/src/Storage/DenormalizingStorage.php +++ b/src/Storage/DenormalizingStorage.php @@ -19,12 +19,25 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +/** + * @template T of object + */ final class DenormalizingStorage implements Storage { private DenormalizerInterface $denormalizer; + /** + * @var Storage + */ private Storage $storage; + /** + * @var class-string + */ private string $class; + /** + * @param Storage $storage + * @param class-string $class + */ public function __construct(DenormalizerInterface $denormalizer, Storage $storage, string $class) { $this->denormalizer = $denormalizer; @@ -32,22 +45,45 @@ public function __construct(DenormalizerInterface $denormalizer, Storage $storag $this->class = $class; } - public static function resolveOptions(array $options): array - { - throw new \LogicException('Does not take options'); - } - + /** + * @return iterable + */ public function load(): iterable { foreach ($this->storage->load() as $id => $item) { - try { - yield $id => $this->denormalizer->denormalize($item, $this->class, null, [ - AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, - ]); - } catch (ExtraAttributesException $extraAttributesException) { - throw UnexpectedAttributeException::newSelf($id, $extraAttributesException->getMessage()); + if (\is_object($item)) { + yield $id => $item; + continue; } + + yield $id => $this->denormalize($id, $item); + } + } + + /** + * @return T + */ + public function get(string $id): object + { + $item = $this->storage->get($id); + if (\is_object($item)) { + return $item; + } + + return $this->denormalize($id, $item); + } + + /** + * @return T + */ + private function denormalize(string $id, array $data): object + { + try { + return $this->denormalizer->denormalize($data, $this->class, null, [ + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, + ]); + } catch (ExtraAttributesException $extraAttributesException) { + throw UnexpectedAttributeException::newSelf($id, $extraAttributesException->getMessage()); } - yield; } } diff --git a/src/Storage/FilesystemStorage.php b/src/Storage/FilesystemStorage.php index ee2ed59..5064992 100644 --- a/src/Storage/FilesystemStorage.php +++ b/src/Storage/FilesystemStorage.php @@ -14,22 +14,24 @@ namespace Sigwin\YASSG\Storage; use Sigwin\YASSG\FileDecoder; -use Sigwin\YASSG\Storage; +use Sigwin\YASSG\StorageWithOptions; use Symfony\Component\Finder\Finder; use Symfony\Component\OptionsResolver\OptionsResolver; -final class FilesystemStorage implements Storage +final class FilesystemStorage implements StorageWithOptions { private FileDecoder $decoder; + private string $root; private Finder $finder; - public function __construct(FileDecoder $decoder, array $paths, ?array $names = null) + public function __construct(FileDecoder $decoder, string $root, ?array $names = null) { $this->decoder = $decoder; + $this->root = rtrim($root, \DIRECTORY_SEPARATOR); $this->finder = new Finder(); $this->finder ->files() - ->in($paths); + ->in($root); if ($names !== null) { $this->finder->name($names); @@ -39,23 +41,35 @@ public function __construct(FileDecoder $decoder, array $paths, ?array $names = public function load(): iterable { foreach ($this->finder as $file) { - $id = $file->getRealPath(); - if ($this->decoder->supports($file) === false) { - throw new \RuntimeException(sprintf('Decoder does not know how to decode %1$s file', $id)); - } + /** @var string $path */ + $path = $file->getRealPath(); - yield $id => $this->decoder->decode($file); + yield str_replace($this->root, '', $path) => $this->decode($file); } } + public function get(string $id): array + { + return $this->decode(new \SplFileObject($this->root.$id)); + } + public static function resolveOptions(array $options): array { $resolver = new OptionsResolver(); - $resolver->setDefined(['paths', 'names']); - $resolver->setRequired(['paths']); - $resolver->setAllowedTypes('paths', ['array', 'string']); + $resolver->setDefined(['root', 'names']); + $resolver->setRequired(['root']); + $resolver->setAllowedTypes('root', 'string'); $resolver->setAllowedTypes('names', ['array', 'string']); return $resolver->resolve($options); } + + private function decode(\SplFileInfo $file): array + { + if ($this->decoder->supports($file) === false) { + throw new \RuntimeException(sprintf('Decoder does not know how to decode %1$s file', $file->getRealPath())); + } + + return $this->decoder->decode($file); + } } diff --git a/src/Storage/MemoryStorage.php b/src/Storage/MemoryStorage.php index a7af022..429e1e7 100644 --- a/src/Storage/MemoryStorage.php +++ b/src/Storage/MemoryStorage.php @@ -13,11 +13,18 @@ namespace Sigwin\YASSG\Storage; -use Sigwin\YASSG\Storage; +use Sigwin\YASSG\StorageWithOptions; use Symfony\Component\OptionsResolver\OptionsResolver; -final class MemoryStorage implements Storage +/** + * @template T of object + * @implements StorageWithOptions + */ +final class MemoryStorage implements StorageWithOptions { + /** + * @var array + */ private array $values; public function __construct(array $values) @@ -38,4 +45,9 @@ public function load(): iterable { return $this->values; } + + public function get(string $id): array + { + return $this->values[$id]; + } } diff --git a/src/StorageWithOptions.php b/src/StorageWithOptions.php new file mode 100644 index 0000000..2288c71 --- /dev/null +++ b/src/StorageWithOptions.php @@ -0,0 +1,23 @@ + + */ +interface StorageWithOptions extends Storage +{ + public static function resolveOptions(array $options): array; +} diff --git a/tests/functional/site/config/packages/yassg_databases.yaml b/tests/functional/site/config/packages/yassg_databases.yaml index c9174de..1466089 100644 --- a/tests/functional/site/config/packages/yassg_databases.yaml +++ b/tests/functional/site/config/packages/yassg_databases.yaml @@ -4,16 +4,14 @@ sigwin_yassg: class: Sigwin\YASSG\Test\Functional\Site\Model\Product storage: filesystem options: - paths: - - "%sigwin_yassg.base_dir%/content/products" + root: "%sigwin_yassg.base_dir%/content/products" names: - "*.yaml" categories: class: Sigwin\YASSG\Test\Functional\Site\Model\Category storage: filesystem options: - paths: - - "%sigwin_yassg.base_dir%/content/categories" + root: "%sigwin_yassg.base_dir%/content/categories" names: - "*.yaml" locale: diff --git a/tests/functional/site/content/categories/category1.yaml b/tests/functional/site/content/categories/category1.yaml index 959c3f5..63a0886 100644 --- a/tests/functional/site/content/categories/category1.yaml +++ b/tests/functional/site/content/categories/category1.yaml @@ -1 +1,5 @@ slug: category1 +name: + de: Kategorie 1 + en: Category 1 + hr: Kategorija 1 diff --git a/tests/functional/site/content/products/test2.en.yaml b/tests/functional/site/content/products/test2.en.yaml index e908979..2dd81bf 100644 --- a/tests/functional/site/content/products/test2.en.yaml +++ b/tests/functional/site/content/products/test2.en.yaml @@ -8,9 +8,5 @@ name: hr: Primjer 2 index: 2 categories: - - - slug: category3 - name: - de: Kategorie 3 - en: Category 3 - hr: Kategorija 3 + - '@=yassg_get("categories", "/category1.yaml")' + - '@=yassg_get("categories", "/category1.yaml")' diff --git a/tests/functional/site/src/Model/Category.php b/tests/functional/site/src/Model/Category.php index a6f22f1..1e58721 100644 --- a/tests/functional/site/src/Model/Category.php +++ b/tests/functional/site/src/Model/Category.php @@ -20,4 +20,9 @@ final class Category public string $slug; #[Localized] public string $name; + + public function random(): int + { + return mt_rand(); + } } diff --git a/tests/functional/site/templates/pages/homepage.html.twig b/tests/functional/site/templates/pages/homepage.html.twig index f1d3874..9ac2f11 100644 --- a/tests/functional/site/templates/pages/homepage.html.twig +++ b/tests/functional/site/templates/pages/homepage.html.twig @@ -5,6 +5,8 @@ {% set products = yassg_find_all('products', {condition: '"category3" in item.categories.slug', sort: {'item.name': 'desc'}}) %} +{% set category1 = yassg_get('categories', '/category1.yaml') %} + {% block title %}Homepage{% endblock %} {% block body %} @@ -27,6 +29,9 @@ {% endfor %} + +

{{ category1.name }}

+

{{ category1.random() }}

diff --git a/tests/functional/site/templates/pages/product.html.twig b/tests/functional/site/templates/pages/product.html.twig index b68c343..aec23ab 100644 --- a/tests/functional/site/templates/pages/product.html.twig +++ b/tests/functional/site/templates/pages/product.html.twig @@ -10,7 +10,14 @@

{{ product.name }}

-
+ +
+ {% for category in product.categories %} +
{{ category.name }}
+
{{ category.random() }}
+ {% endfor %} +
+
{% endblock %}