From 1917b8d6f6a9fe9374d480a14d48879094c3da5e Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 08:33:07 +0100 Subject: [PATCH 01/30] Add first draft of attributes-based implementation --- composer.json | 3 +- config/services.yaml | 5 +- src/Cache/DebugCache.php | 47 +++++++++ src/Cache/DebugCacheFactory.php | 32 ++++++ src/Command/DebugCommand.php | 45 +++++++++ src/Component/AbstractComponent.php | 8 +- src/Component/Config/ComponentType.php | 2 +- src/Context/ComponentContext.php | 4 +- .../Component/ComponentDefinition.php | 51 ++++++++++ .../Component/ComponentDefinitionFactory.php | 98 +++++++++++++++++++ .../Component/ComponentDefinitionRegistry.php | 57 +++++++++++ .../Component/Reflection/ReflectionHelper.php | 93 ++++++++++++++++++ src/Definition/Field/FieldDefinition.php | 33 +++++++ ...ollectComponentDefinitionsCompilerPass.php | 80 +++++++++++++++ .../DuplicateComponentKeyException.php | 9 ++ .../InvalidComponentDefinitionException.php | 9 ++ src/Exception/Story/InvalidDataException.php | 4 +- src/Field/Collection/FieldCollection.php | 12 +-- src/Field/Definition/AbstractField.php | 4 +- src/Field/FieldDefinition.php | 8 ++ src/Field/FieldDefinitionInterface.php | 48 --------- src/Field/Group/AbstractGroupingElement.php | 6 +- src/Field/Group/CompositeField.php | 4 +- src/Field/Helper/FieldDefinitionHelper.php | 4 +- src/Field/NestedFieldDefinitionInterface.php | 2 +- src/Manager/ComponentManager.php | 85 +++++++++++++++- src/Mapping/Field/AbstractField.php | 31 ++++++ src/Mapping/Field/TextField.php | 45 +++++++++ .../FieldAttributeInterface.php | 12 +++ .../FieldAttribute/WithDescription.php | 18 ++++ src/Mapping/FieldAttribute/WithValidation.php | 21 ++++ src/Mapping/Storyblok.php | 18 ++++ src/TorrStoryblokBundle.php | 3 + src/Validator/DataValidator.php | 4 +- src/Visitor/ComponentDataVisitorInterface.php | 4 +- src/Visitor/DataVisitorInterface.php | 4 +- 36 files changed, 827 insertions(+), 86 deletions(-) create mode 100644 src/Cache/DebugCache.php create mode 100644 src/Cache/DebugCacheFactory.php create mode 100644 src/Command/DebugCommand.php create mode 100644 src/Definition/Component/ComponentDefinition.php create mode 100644 src/Definition/Component/ComponentDefinitionFactory.php create mode 100644 src/Definition/Component/ComponentDefinitionRegistry.php create mode 100644 src/Definition/Component/Reflection/ReflectionHelper.php create mode 100644 src/Definition/Field/FieldDefinition.php create mode 100644 src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php create mode 100644 src/Exception/Component/DuplicateComponentKeyException.php create mode 100644 src/Exception/Component/InvalidComponentDefinitionException.php create mode 100644 src/Field/FieldDefinition.php delete mode 100644 src/Field/FieldDefinitionInterface.php create mode 100644 src/Mapping/Field/AbstractField.php create mode 100644 src/Mapping/Field/TextField.php create mode 100644 src/Mapping/FieldAttribute/FieldAttributeInterface.php create mode 100644 src/Mapping/FieldAttribute/WithDescription.php create mode 100644 src/Mapping/FieldAttribute/WithValidation.php create mode 100644 src/Mapping/Storyblok.php diff --git a/composer.json b/composer.json index 60f3cd0..c670ae4 100644 --- a/composer.json +++ b/composer.json @@ -10,10 +10,11 @@ } ], "require": { - "php": ">= 8.1", + "php": ">= 8.2", "21torr/bundle-helpers": "^2.1.2", "21torr/cli": "^1.0", "psr/log": "^3.0", + "symfony/cache-contracts": "^3.3", "symfony/console": "^6.1", "symfony/http-client": "^6.1", "symfony/lock": "^6.2", diff --git a/config/services.yaml b/config/services.yaml index 244533d..8df91c1 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -19,6 +19,5 @@ services: $managementToken: !abstract set via config $contentToken: !abstract set via config - - Torr\Storyblok\Manager\ComponentManager: - $components: !tagged_locator { tag: 'storyblok.component.definition', default_index_method: 'getKey' } + Torr\Storyblok\Cache\DebugCacheFactory: + $isDebug: '%kernel.debug%' diff --git a/src/Cache/DebugCache.php b/src/Cache/DebugCache.php new file mode 100644 index 0000000..1bf054d --- /dev/null +++ b/src/Cache/DebugCache.php @@ -0,0 +1,47 @@ +itemGenerator = $itemGenerator; + } + + + /** + * @return DataType + */ + public function get () : mixed + { + + if ($this->isDebug) + { + if ($this->hasLocaleCache) + { + return $this->localCache; + } + + $this->hasLocaleCache = true; + return $this->localCache = ($this->itemGenerator)(); + } + + return $this->cache->get($this->cacheKey, $this->itemGenerator); + } +} diff --git a/src/Cache/DebugCacheFactory.php b/src/Cache/DebugCacheFactory.php new file mode 100644 index 0000000..2cd1c1c --- /dev/null +++ b/src/Cache/DebugCacheFactory.php @@ -0,0 +1,32 @@ +cache, + $cacheKey, + $itemGenerator, + $this->isDebug, + ); + } +} diff --git a/src/Command/DebugCommand.php b/src/Command/DebugCommand.php new file mode 100644 index 0000000..d6f6b1a --- /dev/null +++ b/src/Command/DebugCommand.php @@ -0,0 +1,45 @@ +title("Storyblok: Debug"); + + $definitions = $this->componentManager->getDefinitions(); + dump($definitions); + + foreach ($definitions->getComponents() as $component) + { + $io->section($component->definition->name); + dump($component->generateManagementApiData()); + } + + return self::SUCCESS; + } +} diff --git a/src/Component/AbstractComponent.php b/src/Component/AbstractComponent.php index 96130b8..55c0a48 100644 --- a/src/Component/AbstractComponent.php +++ b/src/Component/AbstractComponent.php @@ -13,7 +13,7 @@ use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Field\Collection\FieldCollection; use Torr\Storyblok\Field\Data\Helper\InlinedTransformedData; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; use Torr\Storyblok\Management\ManagementApiData; use Torr\Storyblok\Story\Story; use Torr\Storyblok\Visitor\ComponentDataVisitorInterface; @@ -44,7 +44,7 @@ protected function configureComponent () : ComponentDefinition /** * Configures the fields in this component * - * @return array + * @return array */ abstract protected function configureFields () : array; @@ -223,7 +223,7 @@ final protected function getFields () : FieldCollection /** * Normalizes the fields for usage in the management API * - * @param array $fields + * @param array $fields */ private function normalizeFields ( array $fields, @@ -285,7 +285,7 @@ final public function toManagementApiData () : array "real_name" => static::getKey(), "color" => $definition->iconBackgroundColor, "icon" => $definition->icon?->value, - ...$this->getComponentType()->toManagementApiData(), + ...$this->getComponentType()->generateManagementApiData(), ]; } } diff --git a/src/Component/Config/ComponentType.php b/src/Component/Config/ComponentType.php index 7d7ad18..f9c7267 100644 --- a/src/Component/Config/ComponentType.php +++ b/src/Component/Config/ComponentType.php @@ -10,7 +10,7 @@ enum ComponentType case Universal; - public function toManagementApiData () : array + public function generateManagementApiData () : array { return match ($this) { diff --git a/src/Context/ComponentContext.php b/src/Context/ComponentContext.php index 1d0e59d..89592df 100644 --- a/src/Context/ComponentContext.php +++ b/src/Context/ComponentContext.php @@ -7,7 +7,7 @@ use Symfony\Contracts\Service\Attribute\Required; use Torr\Storyblok\Api\Transformer\StoryblokIdSlugMapper; use Torr\Storyblok\Component\AbstractComponent; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; use Torr\Storyblok\Image\ImageDimensionsExtractor; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Transformer\DataTransformer; @@ -49,7 +49,7 @@ public function setStoryblokIdSlugMapper (StoryblokIdSlugMapper $storyblokIdSlug */ public function ensureDataIsValid ( array $contentPath, - FieldDefinitionInterface $field, + FieldDefinition $field, mixed $data, array $constraints, ) : void diff --git a/src/Definition/Component/ComponentDefinition.php b/src/Definition/Component/ComponentDefinition.php new file mode 100644 index 0000000..5d95c31 --- /dev/null +++ b/src/Definition/Component/ComponentDefinition.php @@ -0,0 +1,51 @@ + */ + public array $fields, + ) + { + if (null !== $this->definition->previewField && !\array_key_exists($this->definition->previewField, $this->fields)) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Can't use unknown field '%s' as preview field in story '%s'", + $this->definition->previewField, + $this->storyClass, + )); + } + } + + + public function generateManagementApiData () : array + { + $fields = []; + + foreach ($this->fields as $field) + { + $fieldData = $field->generateManagementApiData(); + $fieldData["preview_field"] = $field->field->key === $this->definition->previewField; + + $fields[$field->field->key] = $fieldData; + } + + return [ + "name" => $this->definition->key, + "real_name" => $this->definition->key, + "display_name" => $this->definition->name, + "schema" => $fields, + ...$this->definition->type->generateManagementApiData(), + ]; + } +} diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php new file mode 100644 index 0000000..7f5ccae --- /dev/null +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -0,0 +1,98 @@ + $storyblokClass) + { + $result[$key] = $this->createDefinition($storyblokClass); + } + + return new ComponentDefinitionRegistry($result); + } + + + /** + * @param class-string $storyblokClass + */ + private function createDefinition ( + string $storyblokClass, + ) : ComponentDefinition + { + try + { + $reflectionClass = new \ReflectionClass($storyblokClass); + $blok = $this->helper->getRequiredSingleAttribute($reflectionClass, Storyblok::class); + + return new ComponentDefinition( + $blok, + $storyblokClass, + $this->createFieldDefinitions($reflectionClass), + ); + } + catch (\ReflectionException $exception) + { + throw new InvalidComponentDefinitionException( + message: \sprintf( + "Invalid component definition: %s", + $exception->getMessage(), + ), + previous: $exception, + ); + } + } + + /** + * @return array + */ + private function createFieldDefinitions (\ReflectionClass $class) : array + { + $definitions = []; + + foreach ($class->getProperties() as $reflectionProperty) + { + if ($reflectionProperty->isStatic()) + { + continue; + } + + $fieldDefinition = $this->helper->getOptionalSingleAttribute($reflectionProperty, AbstractField::class); + + if (null === $fieldDefinition) + { + continue; + } + + $definitions[$fieldDefinition->key] = new FieldDefinition( + $fieldDefinition, + $this->helper->getAttributes($reflectionProperty, FieldAttributeInterface::class), + ); + } + + return $definitions; + } +} diff --git a/src/Definition/Component/ComponentDefinitionRegistry.php b/src/Definition/Component/ComponentDefinitionRegistry.php new file mode 100644 index 0000000..7eeb8dc --- /dev/null +++ b/src/Definition/Component/ComponentDefinitionRegistry.php @@ -0,0 +1,57 @@ + */ + private array $componentsByTags; + + + public function __construct ( + /** @var array */ + private array $definitions, + ) + { + $this->componentsByTags = $this->indexTags($this->definitions); + } + + + /** + * @param ComponentDefinition[] $definitions + */ + private function indexTags (array $definitions) : array + { + $index = []; + + foreach ($definitions as $definition) + { + foreach ($definition->definition->tags as $tag) + { + $tag = $tag instanceof \BackedEnum + ? $tag->value + : $tag; + $index[$tag][] = $definition; + } + } + + return $index; + } + + + /** + * @return ComponentDefinition[] + */ + public function getComponentsByTag (string $tag) : array + { + return $this->componentsByTags[$tag] ?? []; + } + + /** + * @return ComponentDefinition[] + */ + public function getComponents () : array + { + return $this->definitions; + } +} diff --git a/src/Definition/Component/Reflection/ReflectionHelper.php b/src/Definition/Component/Reflection/ReflectionHelper.php new file mode 100644 index 0000000..aef028f --- /dev/null +++ b/src/Definition/Component/Reflection/ReflectionHelper.php @@ -0,0 +1,93 @@ + $attributeClass + * @return AttributeType + * + * @throws InvalidComponentDefinitionException + */ + public function getRequiredSingleAttribute ( + \ReflectionClass|\ReflectionProperty $element, + string $attributeClass, + ) : object + { + $attribute = $this->getOptionalSingleAttribute($element, $attributeClass); + + if (null === $attribute) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Could not find required attribute of type '%s' on class '%s'.", + $attributeClass, + $element->getName(), + )); + } + + return $attribute; + } + + + /** + * Instantiates and returns a single attribute + * + * @template AttributeType of object + * + * @param class-string $attributeClass + * @return AttributeType|null + * + * @throws InvalidComponentDefinitionException + */ + public function getOptionalSingleAttribute ( + \ReflectionClass|\ReflectionProperty $element, + string $attributeClass, + ) : ?object + { + $attributes = $this->getAttributes($element, $attributeClass); + + if (\count($attributes) > 1) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Found multiple instances of attribute '%s', but expected only one.", + $attributeClass, + )); + } + + return $attributes[0] ?? null; + } + + + /** + * @template AttributeType of object + * + * @param class-string $attributeClass + * @return array + */ + public function getAttributes ( + \ReflectionClass|\ReflectionProperty $element, + string $attributeClass, + ) : array + { + $result = []; + + foreach ($element->getAttributes() as $reflectionAttribute) + { + if (\is_a($reflectionAttribute->getName(), $attributeClass, true)) + { + /** @var AttributeType $attribute */ + $attribute = $reflectionAttribute->newInstance(); + $result[] = $attribute; + } + } + + return $result; + } +} diff --git a/src/Definition/Field/FieldDefinition.php b/src/Definition/Field/FieldDefinition.php new file mode 100644 index 0000000..e2b34e1 --- /dev/null +++ b/src/Definition/Field/FieldDefinition.php @@ -0,0 +1,33 @@ +field->generateManagementApiData(); + + foreach ($this->attributes as $attribute) + { + foreach ($attribute->managementApiData as $key => $value) + { + $data[$key] = $value; + } + } + + return $data; + } +} diff --git a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php new file mode 100644 index 0000000..58e6513 --- /dev/null +++ b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php @@ -0,0 +1,80 @@ +getDefinitions() as $id => $definition) + { + /** @var class-string|null $class */ + $class = $definition->getClass(); + + if (null === $class || !\str_contains($class, "Storyblok")) + { + continue; + } + + $attribute = $this->extractKey($class); + + if (null === $attribute) + { + continue; + } + + if (\array_key_exists($attribute->key, $definitions)) + { + throw new DuplicateComponentKeyException(\sprintf( + "Found multiple story definitions for key '%s'", + $attribute->key, + )); + } + + $definitions[$attribute->key] = $class; + $container->removeDefinition($id); + } + + $container->getDefinition(ComponentManager::class) + ->setArgument('$storyComponents', $definitions); + } + + + /** + * Extracts the key of the class + * + * @param class-string $class + */ + private function extractKey (string $class) : ?Storyblok + { + try + { + $reflectionClass = new \ReflectionClass($class); + $reflectionAttribute = $reflectionClass->getAttributes(Storyblok::class)[0] ?? null; + + if (null === $reflectionAttribute) + { + return null; + } + + $attribute = $reflectionAttribute->newInstance(); + \assert($attribute instanceof Storyblok); + return $attribute; + } + catch (\ReflectionException $e) + { + return null; + } + } +} diff --git a/src/Exception/Component/DuplicateComponentKeyException.php b/src/Exception/Component/DuplicateComponentKeyException.php new file mode 100644 index 0000000..6b66904 --- /dev/null +++ b/src/Exception/Component/DuplicateComponentKeyException.php @@ -0,0 +1,9 @@ + $contentPath */ public readonly ?array $contentPath = null, - public readonly FieldDefinitionInterface|AbstractComponent|null $field = null, + public readonly FieldDefinition|AbstractComponent|null $field = null, public readonly mixed $data = null, public readonly ?ConstraintViolationListInterface $violations = null, ?\Throwable $previous = null, diff --git a/src/Field/Collection/FieldCollection.php b/src/Field/Collection/FieldCollection.php index 7907367..c395117 100644 --- a/src/Field/Collection/FieldCollection.php +++ b/src/Field/Collection/FieldCollection.php @@ -3,17 +3,17 @@ namespace Torr\Storyblok\Field\Collection; use Torr\Storyblok\Exception\Story\UnknownFieldException; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; use Torr\Storyblok\Field\Helper\FieldDefinitionHelper; use Torr\Storyblok\Field\NestedFieldDefinitionInterface; final class FieldCollection { - /** @var array */ + /** @var array */ private array $allFields = []; public function __construct ( - /** @var array $rootFields */ + /** @var array $rootFields */ private readonly array $rootFields, ) { @@ -22,7 +22,7 @@ public function __construct ( } /** - * @param array $fields + * @param array $fields */ private function indexFields (array $fields) : void { @@ -39,7 +39,7 @@ private function indexFields (array $fields) : void /** - * @return array + * @return array */ public function getRootFields () : array { @@ -49,7 +49,7 @@ public function getRootFields () : array /** * Returns a single transformable field */ - public function getField (string $key) : FieldDefinitionInterface + public function getField (string $key) : FieldDefinition { $field = $this->allFields[$key] ?? null; diff --git a/src/Field/Definition/AbstractField.php b/src/Field/Definition/AbstractField.php index e2ddf86..1a148b2 100644 --- a/src/Field/Definition/AbstractField.php +++ b/src/Field/Definition/AbstractField.php @@ -2,7 +2,7 @@ namespace Torr\Storyblok\Field\Definition; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Management\ManagementApiData; @@ -11,7 +11,7 @@ * * @internal */ -abstract class AbstractField implements FieldDefinitionInterface +abstract class AbstractField implements FieldDefinition { private ?bool $canSync = false; private ?bool $useAsAdminDisplayName = false; diff --git a/src/Field/FieldDefinition.php b/src/Field/FieldDefinition.php new file mode 100644 index 0000000..c363f64 --- /dev/null +++ b/src/Field/FieldDefinition.php @@ -0,0 +1,8 @@ + $fields + * @param array $fields */ public function __construct ( string $label, - /** @var array $fields */ + /** @var array $fields */ protected readonly array $fields, ) { diff --git a/src/Field/Group/CompositeField.php b/src/Field/Group/CompositeField.php index f86dbcd..ef35e92 100644 --- a/src/Field/Group/CompositeField.php +++ b/src/Field/Group/CompositeField.php @@ -4,7 +4,7 @@ use Symfony\Component\String\Slugger\AsciiSlugger; use Torr\Storyblok\Context\ComponentContext; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Visitor\DataVisitorInterface; @@ -41,7 +41,7 @@ public function __construct ( /** * Configures the fields * - * @return array + * @return array */ abstract protected function configureFields () : array; diff --git a/src/Field/Helper/FieldDefinitionHelper.php b/src/Field/Helper/FieldDefinitionHelper.php index f2226c7..0a4f4c0 100644 --- a/src/Field/Helper/FieldDefinitionHelper.php +++ b/src/Field/Helper/FieldDefinitionHelper.php @@ -4,14 +4,14 @@ use Torr\Storyblok\Exception\InvalidComponentConfigurationException; use Torr\Storyblok\Field\Definition\AbstractField; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; final class FieldDefinitionHelper { /** * Ensures that there is a only a single field with an admin display preview * - * @param iterable $fields + * @param iterable $fields */ public static function ensureMaximumOneAdminDisplayName (iterable $fields) : void { diff --git a/src/Field/NestedFieldDefinitionInterface.php b/src/Field/NestedFieldDefinitionInterface.php index a9cb4b1..4b8345f 100644 --- a/src/Field/NestedFieldDefinitionInterface.php +++ b/src/Field/NestedFieldDefinitionInterface.php @@ -7,7 +7,7 @@ interface NestedFieldDefinitionInterface /** * Returns the nested fields * - * @return FieldDefinitionInterface[] + * @return FieldDefinition[] */ public function getNestedFields () : array; } diff --git a/src/Manager/ComponentManager.php b/src/Manager/ComponentManager.php index eb7f374..437ac7c 100644 --- a/src/Manager/ComponentManager.php +++ b/src/Manager/ComponentManager.php @@ -3,10 +3,17 @@ namespace Torr\Storyblok\Manager; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; -use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Contracts\Cache\CacheInterface; +use Torr\Storyblok\Cache\DebugCache; +use Torr\Storyblok\Cache\DebugCacheFactory; use Torr\Storyblok\Component\AbstractComponent; +use Torr\Storyblok\Definition\Component\ComponentDefinition; +use Torr\Storyblok\Definition\Component\ComponentDefinitionFactory; +use Torr\Storyblok\Definition\Component\ComponentDefinitionRegistry; +use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; use Torr\Storyblok\Exception\Component\UnknownStoryTypeException; +use Torr\Storyblok\Mapping\Storyblok; use Torr\Storyblok\Story\Story; /** @@ -14,11 +21,28 @@ */ class ComponentManager { + private const CACHE_KEY = "storyblok.definitions"; + + /** @var DebugCache */ + private DebugCache $definitionCache; + /** */ public function __construct ( - private readonly ServiceLocator $components, - ) {} + /** @var array */ + array $storyComponents, + ComponentDefinitionFactory $definitionFactory, + DebugCacheFactory $debugCacheFactory, + ) + { + $this->definitionCache = $debugCacheFactory->createCache( + self::CACHE_KEY, + fn () => $definitionFactory->generateAllDefinitions($storyComponents), + ); + } + + + /** * @return array @@ -123,4 +147,59 @@ public function getComponent (string $key) : AbstractComponent ); } } + + /** + */ + public function getDefinitions () : ComponentDefinitionRegistry + { + return $this->definitionCache->get(); + } + + /** + * Returns the definition of the component + */ + public function getDefinition (string $key) : ComponentDefinition + { + if (\array_key_exists($key, $this->definitions)) + { + return $this->definitions[$key]; + } + + if (!\array_key_exists($key, $this->storyComponents)) + { + throw new UnknownComponentKeyException( + message: \sprintf( + "Unknown component type: %s", + $key, + ), + componentKey: $key, + ); + } + + try + { + $reflectionClass = new \ReflectionClass($this->storyComponents[$key]); + $attribute = $reflectionClass->getAttributes(Storyblok::class)[0] ?? null; + + \assert(null !== $attribute); + $definition = $attribute->newInstance(); + \assert($definition instanceof Storyblok); + + return $this->definitions[$key] = new ComponentDefinition( + $definition, + $this->storyComponents[$key], + ); + } + catch (\ReflectionException $exception) + { + throw new InvalidComponentDefinitionException( + message: \sprintf( + "Invalid component definition: %s", + $exception->getMessage(), + ), + previous: $exception, + ); + } + } + } diff --git a/src/Mapping/Field/AbstractField.php b/src/Mapping/Field/AbstractField.php new file mode 100644 index 0000000..3399903 --- /dev/null +++ b/src/Mapping/Field/AbstractField.php @@ -0,0 +1,31 @@ + $this->getInternalStoryblokType()->value, + "display_name" => $this->label, + "default_value" => $this->defaultValue, + ]; + } + + /** + * Returns the internal storyblok type + */ + abstract protected function getInternalStoryblokType () : FieldType; +} diff --git a/src/Mapping/Field/TextField.php b/src/Mapping/Field/TextField.php new file mode 100644 index 0000000..5beea16 --- /dev/null +++ b/src/Mapping/Field/TextField.php @@ -0,0 +1,45 @@ +multiline + ? FieldType::TextArea + : FieldType::Text; + } + + /** + * @inheritDoc + */ + public function generateManagementApiData () : array + { + return \array_replace( + parent::generateManagementApiData(), + [ + "rtl" => $this->isRightToLeft, + "max_length" => $this->maxLength, + ], + ); + } +} diff --git a/src/Mapping/FieldAttribute/FieldAttributeInterface.php b/src/Mapping/FieldAttribute/FieldAttributeInterface.php new file mode 100644 index 0000000..fccfdec --- /dev/null +++ b/src/Mapping/FieldAttribute/FieldAttributeInterface.php @@ -0,0 +1,12 @@ + $this->description, + "tooltip" => $this->showAsTooltip, + ]); + } +} diff --git a/src/Mapping/FieldAttribute/WithValidation.php b/src/Mapping/FieldAttribute/WithValidation.php new file mode 100644 index 0000000..f82895f --- /dev/null +++ b/src/Mapping/FieldAttribute/WithValidation.php @@ -0,0 +1,21 @@ + $required, + "regex" => $regexp, + ]); + } +} diff --git a/src/Mapping/Storyblok.php b/src/Mapping/Storyblok.php new file mode 100644 index 0000000..13adfe9 --- /dev/null +++ b/src/Mapping/Storyblok.php @@ -0,0 +1,18 @@ + */ + public array $tags = [], + public ?string $previewField = null, + ) {} +} diff --git a/src/TorrStoryblokBundle.php b/src/TorrStoryblokBundle.php index 1e3129f..1f31fd7 100644 --- a/src/TorrStoryblokBundle.php +++ b/src/TorrStoryblokBundle.php @@ -7,6 +7,7 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use Torr\Storyblok\Component\AbstractComponent; use Torr\Storyblok\Config\StoryblokConfig; +use Torr\Storyblok\DependencyInjection\CollectComponentDefinitionsCompilerPass; use Torr\Storyblok\DependencyInjection\StoryblokBundleConfiguration; use Torr\Storyblok\DependencyInjection\StoryblokBundleExtension; @@ -36,6 +37,8 @@ static function (array $config, ContainerBuilder $container) : void */ public function build (ContainerBuilder $container) : void { + $container->addCompilerPass(new CollectComponentDefinitionsCompilerPass()); + $container->registerForAutoconfiguration(AbstractComponent::class) ->addTag("storyblok.component.definition"); } diff --git a/src/Validator/DataValidator.php b/src/Validator/DataValidator.php index 13a31b0..4e569c6 100644 --- a/src/Validator/DataValidator.php +++ b/src/Validator/DataValidator.php @@ -7,7 +7,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Torr\Storyblok\Component\AbstractComponent; use Torr\Storyblok\Exception\Story\InvalidDataException; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; /** * @final @@ -31,7 +31,7 @@ public function __construct ( */ public function ensureDataIsValid ( array $contentPath, - FieldDefinitionInterface|AbstractComponent|null $field, + FieldDefinition|AbstractComponent|null $field, mixed $data, array $constraints, ) : void diff --git a/src/Visitor/ComponentDataVisitorInterface.php b/src/Visitor/ComponentDataVisitorInterface.php index 03e5e62..2c8f272 100644 --- a/src/Visitor/ComponentDataVisitorInterface.php +++ b/src/Visitor/ComponentDataVisitorInterface.php @@ -3,7 +3,7 @@ namespace Torr\Storyblok\Visitor; use Torr\Storyblok\Component\AbstractComponent; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; interface ComponentDataVisitorInterface extends DataVisitorInterface { @@ -11,7 +11,7 @@ interface ComponentDataVisitorInterface extends DataVisitorInterface * If a data visitor is given, it will be called for every field with the field definition or component and data. */ public function onDataVisit ( - FieldDefinitionInterface|AbstractComponent $field, + FieldDefinition|AbstractComponent $field, mixed $data, ) : void; } diff --git a/src/Visitor/DataVisitorInterface.php b/src/Visitor/DataVisitorInterface.php index 67d30fe..c166610 100644 --- a/src/Visitor/DataVisitorInterface.php +++ b/src/Visitor/DataVisitorInterface.php @@ -2,7 +2,7 @@ namespace Torr\Storyblok\Visitor; -use Torr\Storyblok\Field\FieldDefinitionInterface; +use Torr\Storyblok\Field\FieldDefinition; interface DataVisitorInterface { @@ -10,7 +10,7 @@ interface DataVisitorInterface * If a data visitor is given, it will be called for every field with the field definition and data. */ public function onDataVisit ( - FieldDefinitionInterface $field, + FieldDefinition $field, mixed $data, ) : void; } From 814d8a74c07702b27fef9bac12d60a4ab5b54a93 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 08:51:16 +0100 Subject: [PATCH 02/30] Move storyblok type and add number field --- src/Mapping/Field/AbstractField.php | 9 +++------ src/Mapping/Field/NumberField.php | 22 ++++++++++++++++++++++ src/Mapping/Field/TextField.php | 19 ++++++++----------- 3 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 src/Mapping/Field/NumberField.php diff --git a/src/Mapping/Field/AbstractField.php b/src/Mapping/Field/AbstractField.php index 3399903..4bb04f3 100644 --- a/src/Mapping/Field/AbstractField.php +++ b/src/Mapping/Field/AbstractField.php @@ -7,25 +7,22 @@ abstract class AbstractField { public function __construct ( + public readonly FieldType $internalStoryblokType, public readonly string $key, public readonly string $label, public readonly mixed $defaultValue = null, ) {} + /** * */ public function generateManagementApiData () : array { return [ - "type" => $this->getInternalStoryblokType()->value, + "type" => $this->internalStoryblokType->value, "display_name" => $this->label, "default_value" => $this->defaultValue, ]; } - - /** - * Returns the internal storyblok type - */ - abstract protected function getInternalStoryblokType () : FieldType; } diff --git a/src/Mapping/Field/NumberField.php b/src/Mapping/Field/NumberField.php new file mode 100644 index 0000000..5d7bd80 --- /dev/null +++ b/src/Mapping/Field/NumberField.php @@ -0,0 +1,22 @@ +multiline - ? FieldType::TextArea - : FieldType::Text; + parent::__construct( + internalStoryblokType: $this->multiline + ? FieldType::TextArea + : FieldType::Text, + key: $key, + label: $label, + defaultValue: $defaultValue, + ); } /** From 0f183b069ebfbec252bc5124b8e34224316fcd51 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 08:55:44 +0100 Subject: [PATCH 03/30] Sort components by name alphabetically --- .../Component/ComponentDefinitionRegistry.php | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Definition/Component/ComponentDefinitionRegistry.php b/src/Definition/Component/ComponentDefinitionRegistry.php index 7eeb8dc..85c16bc 100644 --- a/src/Definition/Component/ComponentDefinitionRegistry.php +++ b/src/Definition/Component/ComponentDefinitionRegistry.php @@ -4,16 +4,29 @@ final readonly class ComponentDefinitionRegistry { - /** @var array */ + /** + * @var array + */ private array $componentsByTags; + /** + * @var array + */ + private array $definitions; - public function __construct ( - /** @var array */ - private array $definitions, - ) + /** + * @param array $definitions + */ + public function __construct (array $definitions) { - $this->componentsByTags = $this->indexTags($this->definitions); + // sort components by name + \uasort( + $definitions, + static fn (ComponentDefinition $left, ComponentDefinition $right) => \strnatcasecmp($left->definition->name, $right->definition->name), + ); + + $this->definitions = $definitions; + $this->componentsByTags = $this->indexTags($definitions); } From 3237b38442fa2bd19700d6cd6afd1481b4623969 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 13:39:00 +0100 Subject: [PATCH 04/30] Add first draft of story hydration --- composer.json | 1 + src/Command/DebugCommand.php | 29 ++++++- .../Component/ComponentDefinition.php | 19 ++++- .../Component/ComponentDefinitionFactory.php | 5 +- .../Component/ComponentDefinitionRegistry.php | 21 +++++ src/Definition/Field/FieldDefinition.php | 56 +++++++++++- ...ollectComponentDefinitionsCompilerPass.php | 15 +++- .../Story/ComponentWithoutStoryException.php | 7 -- src/Exception/Story/InvalidDataException.php | 1 - src/Field/Definition/NumberField.php | 74 ---------------- src/Field/Definition/TextField.php | 85 ------------------- src/Manager/ComponentManager.php | 42 +-------- .../Normalizer/ComponentNormalizer.php | 15 ++-- src/Manager/Sync/ComponentSync.php | 1 + src/Mapping/Field/AbstractField.php | 21 +++++ src/Mapping/Field/NumberField.php | 80 ++++++++++++++++- src/Mapping/Field/TextField.php | 15 ++++ .../FieldAttributeInterface.php | 12 +++ .../FieldAttribute/WithDescription.php | 1 + src/Mapping/FieldAttribute/WithValidation.php | 24 +++++- src/Mapping/Storyblok.php | 1 + src/Story/Story.php | 64 +------------- src/Story/StoryFactory.php | 73 ++++++++++------ src/TorrStoryblokBundle.php | 1 + src/Validator/DataValidator.php | 2 - src/translations/validator+intl-icu.en.yaml | 5 ++ 26 files changed, 349 insertions(+), 321 deletions(-) delete mode 100644 src/Exception/Story/ComponentWithoutStoryException.php delete mode 100644 src/Field/Definition/NumberField.php delete mode 100644 src/Field/Definition/TextField.php create mode 100644 src/translations/validator+intl-icu.en.yaml diff --git a/composer.json b/composer.json index c670ae4..faec276 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "symfony/console": "^6.1", "symfony/http-client": "^6.1", "symfony/lock": "^6.2", + "symfony/property-access": "^6.3", "symfony/rate-limiter": "^6.2", "symfony/string": "^6.2", "symfony/validator": "^6.1" diff --git a/src/Command/DebugCommand.php b/src/Command/DebugCommand.php index d6f6b1a..5d57235 100644 --- a/src/Command/DebugCommand.php +++ b/src/Command/DebugCommand.php @@ -7,6 +7,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Torr\Cli\Console\Style\TorrStyle; +use Torr\Storyblok\Api\ContentApi; use Torr\Storyblok\Manager\ComponentManager; #[AsCommand("storyblok:debug")] @@ -17,27 +18,47 @@ final class DebugCommand extends Command */ public function __construct ( private readonly ComponentManager $componentManager, + private readonly ContentApi $contentApi, ) { parent::__construct(); } + /** + * @inheritDoc + */ + protected function configure () + { + $this + ->addOption("fetch") + ->addOption("sync"); + } + /** * @inheritDoc */ - protected function execute (InputInterface $input, OutputInterface $output) + protected function execute (InputInterface $input, OutputInterface $output) : int { $io = new TorrStyle($input, $output); $io->title("Storyblok: Debug"); + if ($input->getOption("fetch")) + { + $stories = $this->contentApi->fetchAllStories(null); + dd($stories); + } + $definitions = $this->componentManager->getDefinitions(); dump($definitions); - foreach ($definitions->getComponents() as $component) + if ($input->getOption("sync")) { - $io->section($component->definition->name); - dump($component->generateManagementApiData()); + foreach ($definitions->getComponents() as $component) + { + $io->section($component->definition->name); + dump($component->generateManagementApiData()); + } } return self::SUCCESS; diff --git a/src/Definition/Component/ComponentDefinition.php b/src/Definition/Component/ComponentDefinition.php index 5d95c31..f6045e9 100644 --- a/src/Definition/Component/ComponentDefinition.php +++ b/src/Definition/Component/ComponentDefinition.php @@ -5,6 +5,7 @@ use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Mapping\Storyblok; +use Torr\Storyblok\Story\Story; final readonly class ComponentDefinition { @@ -12,6 +13,7 @@ */ public function __construct ( public Storyblok $definition, + /** @type class-string */ public string $storyClass, /** @type array */ public array $fields, @@ -27,6 +29,20 @@ public function __construct ( } } + /** + */ + public function getDisplayName () : string + { + return $this->definition->name; + } + + /** + */ + public function getKey () : string + { + return $this->definition->key; + } + public function generateManagementApiData () : array { @@ -35,8 +51,6 @@ public function generateManagementApiData () : array foreach ($this->fields as $field) { $fieldData = $field->generateManagementApiData(); - $fieldData["preview_field"] = $field->field->key === $this->definition->previewField; - $fields[$field->field->key] = $fieldData; } @@ -45,6 +59,7 @@ public function generateManagementApiData () : array "real_name" => $this->definition->key, "display_name" => $this->definition->name, "schema" => $fields, + "preview_field" => $this->definition->previewField, ...$this->definition->type->generateManagementApiData(), ]; } diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php index 7f5ccae..d1a8141 100644 --- a/src/Definition/Component/ComponentDefinitionFactory.php +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -9,12 +9,12 @@ use Torr\Storyblok\Mapping\FieldAttribute\FieldAttributeInterface; use Torr\Storyblok\Mapping\Storyblok; -final class ComponentDefinitionFactory +final readonly class ComponentDefinitionFactory { /** */ public function __construct ( - private readonly ReflectionHelper $helper, + private ReflectionHelper $helper, ) {} @@ -89,6 +89,7 @@ private function createFieldDefinitions (\ReflectionClass $class) : array $definitions[$fieldDefinition->key] = new FieldDefinition( $fieldDefinition, + $reflectionProperty->getName(), $this->helper->getAttributes($reflectionProperty, FieldAttributeInterface::class), ); } diff --git a/src/Definition/Component/ComponentDefinitionRegistry.php b/src/Definition/Component/ComponentDefinitionRegistry.php index 85c16bc..c5071ff 100644 --- a/src/Definition/Component/ComponentDefinitionRegistry.php +++ b/src/Definition/Component/ComponentDefinitionRegistry.php @@ -2,6 +2,8 @@ namespace Torr\Storyblok\Definition\Component; +use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; + final readonly class ComponentDefinitionRegistry { /** @@ -52,6 +54,25 @@ private function indexTags (array $definitions) : array } + public function get (string $key) : ComponentDefinition + { + $definition = $this->definitions[$key] ?? null; + + if (null === $definition) + { + throw new UnknownComponentKeyException( + message: \sprintf( + "Could not find component with key '%s'", + $key + ), + componentKey: $key, + ); + } + + return $definition; + } + + /** * @return ComponentDefinition[] */ diff --git a/src/Definition/Field/FieldDefinition.php b/src/Definition/Field/FieldDefinition.php index e2b34e1..ee73fd7 100644 --- a/src/Definition/Field/FieldDefinition.php +++ b/src/Definition/Field/FieldDefinition.php @@ -2,15 +2,22 @@ namespace Torr\Storyblok\Definition\Field; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Mapping\Field\AbstractField; use Torr\Storyblok\Mapping\FieldAttribute\FieldAttributeInterface; +use Torr\Storyblok\Validator\DataValidator; -final class FieldDefinition +final readonly class FieldDefinition { + /** + */ public function __construct ( - public readonly AbstractField $field, + public AbstractField $field, + public string $property, /** @var FieldAttributeInterface[] */ - private readonly array $attributes = [], + private array $attributes = [], ) {} /** @@ -30,4 +37,47 @@ public function generateManagementApiData () : array return $data; } + + + /** + * Validates the given data for the field + * + * @throws InvalidDataException + */ + public function validateData ( + array $contentPath, + DataValidator $validator, + mixed $data, + ) : void + { + $contentPath[] = \sprintf( + $this->field->key !== $this->property + ? "%s (%s)" + : "%s", + $this->property, + $this->field->key, + ); + $constraints = []; + + foreach ($this->attributes as $attribute) + { + foreach ($attribute->getValidationConstraints() as $constraint) + { + $constraints[] = $constraint; + } + } + + // validate attributes first + $validator->ensureDataIsValid( + $contentPath, + $data, + $constraints, + ); + + $this->field->validateData( + $contentPath, + $validator, + $data, + ); + } } diff --git a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php index 58e6513..df034dc 100644 --- a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php +++ b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php @@ -5,8 +5,10 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Torr\Storyblok\Exception\Component\DuplicateComponentKeyException; +use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Mapping\Storyblok; +use Torr\Storyblok\Story\Story; final class CollectComponentDefinitionsCompilerPass implements CompilerPassInterface { @@ -34,11 +36,22 @@ public function process (ContainerBuilder $container) : void continue; } + if (!\is_a($class, Story::class, true)) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Story '%s' must extend %s.", + $class, + Story::class, + )); + } + if (\array_key_exists($attribute->key, $definitions)) { throw new DuplicateComponentKeyException(\sprintf( - "Found multiple story definitions for key '%s'", + "Found multiple story definitions for key '%s'. One in '%s' and one in '%s'", $attribute->key, + $definitions[$attribute->key], + $definition->getClass(), )); } diff --git a/src/Exception/Story/ComponentWithoutStoryException.php b/src/Exception/Story/ComponentWithoutStoryException.php deleted file mode 100644 index 81cd106..0000000 --- a/src/Exception/Story/ComponentWithoutStoryException.php +++ /dev/null @@ -1,7 +0,0 @@ - $contentPath */ public readonly ?array $contentPath = null, - public readonly FieldDefinition|AbstractComponent|null $field = null, public readonly mixed $data = null, public readonly ?ConstraintViolationListInterface $violations = null, ?\Throwable $previous = null, diff --git a/src/Field/Definition/NumberField.php b/src/Field/Definition/NumberField.php deleted file mode 100644 index 74736e9..0000000 --- a/src/Field/Definition/NumberField.php +++ /dev/null @@ -1,74 +0,0 @@ -ensureDataIsValid( - $contentPath, - $this, - $data, - [ - !$this->allowMissingData && $this->required ? new NotNull() : null, - // numbers are always passed as strings - new Type("string"), - new Regex("~^\\d+(\\.\\d+)?$~"), - ], - ); - } - - /** - * @inheritDoc - */ - public function transformData ( - mixed $data, - ComponentContext $context, - array $fullData, - ?DataVisitorInterface $dataVisitor = null, - ) : int|float|null - { - \assert(null === $data || \is_string($data)); - - if (null !== $data && "" !== $data) - { - $transformed = \str_contains($data, ".") - ? (float) $data - : (int) $data; - } - else - { - $transformed = null; - } - - $dataVisitor?->onDataVisit($this, $transformed); - return $transformed; - } -} diff --git a/src/Field/Definition/TextField.php b/src/Field/Definition/TextField.php deleted file mode 100644 index ed9bd39..0000000 --- a/src/Field/Definition/TextField.php +++ /dev/null @@ -1,85 +0,0 @@ -multiline - ? FieldType::TextArea - : FieldType::Text; - } - - /** - * @inheritDoc - */ - protected function toManagementApiData () : array - { - return \array_replace( - parent::toManagementApiData(), - [ - "rtl" => $this->isRightToLeft, - "max_length" => $this->maxLength, - ], - ); - } - - /** - * @inheritDoc - */ - public function validateData (ComponentContext $context, array $contentPath, mixed $data, array $fullData) : void - { - $context->ensureDataIsValid( - $contentPath, - $this, - $data, - [ - !$this->allowMissingData && $this->required ? new NotNull() : null, - new Type("string"), - // We can't validate the length here, as it is not guaranteed if you add - // the max-length after content was added. - ], - ); - } - - /** - * @inheritDoc - */ - public function transformData ( - mixed $data, - ComponentContext $context, - array $fullData, - ?DataVisitorInterface $dataVisitor = null, - ) : ?string - { - \assert(null === $data || \is_string($data)); - $transformed = $context->normalizeOptionalString($data); - - $dataVisitor?->onDataVisit($this, $transformed); - return $transformed; - } -} diff --git a/src/Manager/ComponentManager.php b/src/Manager/ComponentManager.php index 437ac7c..9c15e0f 100644 --- a/src/Manager/ComponentManager.php +++ b/src/Manager/ComponentManager.php @@ -160,46 +160,6 @@ public function getDefinitions () : ComponentDefinitionRegistry */ public function getDefinition (string $key) : ComponentDefinition { - if (\array_key_exists($key, $this->definitions)) - { - return $this->definitions[$key]; - } - - if (!\array_key_exists($key, $this->storyComponents)) - { - throw new UnknownComponentKeyException( - message: \sprintf( - "Unknown component type: %s", - $key, - ), - componentKey: $key, - ); - } - - try - { - $reflectionClass = new \ReflectionClass($this->storyComponents[$key]); - $attribute = $reflectionClass->getAttributes(Storyblok::class)[0] ?? null; - - \assert(null !== $attribute); - $definition = $attribute->newInstance(); - \assert($definition instanceof Storyblok); - - return $this->definitions[$key] = new ComponentDefinition( - $definition, - $this->storyComponents[$key], - ); - } - catch (\ReflectionException $exception) - { - throw new InvalidComponentDefinitionException( - message: \sprintf( - "Invalid component definition: %s", - $exception->getMessage(), - ), - previous: $exception, - ); - } + return $this->getDefinitions()->get($key); } - } diff --git a/src/Manager/Normalizer/ComponentNormalizer.php b/src/Manager/Normalizer/ComponentNormalizer.php index 28e593d..7312c0c 100644 --- a/src/Manager/Normalizer/ComponentNormalizer.php +++ b/src/Manager/Normalizer/ComponentNormalizer.php @@ -5,13 +5,11 @@ use Torr\Cli\Console\Style\TorrStyle; use Torr\Storyblok\Api\Data\ComponentImport; use Torr\Storyblok\Manager\ComponentManager; -use Torr\Storyblok\Manager\Sync\ComponentConfigResolver; final class ComponentNormalizer { public function __construct ( private readonly ComponentManager $componentManager, - private readonly ComponentConfigResolver $componentConfigResolver, ) {} @@ -23,21 +21,24 @@ public function __construct ( public function normalize (TorrStyle $io) : array { $normalized = []; + $definitions = $this->componentManager->getDefinitions(); // normalize everything to check if normalization fails - foreach ($this->componentManager->getAllComponents() as $component) + foreach ($definitions->getComponents() as $component) { + $definition = $component->definition; + $key = \sprintf( "%s (%s) ... ", - $component->getDisplayName(), - $component::getKey(), + $definition->name, + $definition->key, ); $io->write("Normalizing {$key} "); $normalized[$key] = new ComponentImport( - $this->componentConfigResolver->resolveComponentConfig($component->toManagementApiData()), - $component->getComponentGroup(), + $component->generateManagementApiData(), + $definition->group, ); $io->writeln("done ✓"); diff --git a/src/Manager/Sync/ComponentSync.php b/src/Manager/Sync/ComponentSync.php index 67c2341..d31486a 100644 --- a/src/Manager/Sync/ComponentSync.php +++ b/src/Manager/Sync/ComponentSync.php @@ -8,6 +8,7 @@ use Torr\Storyblok\Exception\Api\ApiRequestException; use Torr\Storyblok\Exception\InvalidComponentConfigurationException; use Torr\Storyblok\Exception\Sync\SyncFailedException; +use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Manager\Normalizer\ComponentNormalizer; final class ComponentSync diff --git a/src/Mapping/Field/AbstractField.php b/src/Mapping/Field/AbstractField.php index 4bb04f3..43a13c4 100644 --- a/src/Mapping/Field/AbstractField.php +++ b/src/Mapping/Field/AbstractField.php @@ -3,6 +3,7 @@ namespace Torr\Storyblok\Mapping\Field; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Validator\DataValidator; abstract class AbstractField { @@ -25,4 +26,24 @@ public function generateManagementApiData () : array "default_value" => $this->defaultValue, ]; } + + /** + * Normalizes the data from Storyblok to be more strict + */ + public function normalizeData (mixed $data) : mixed + { + return $data; + } + + + /** + * Validates the given data + */ + public function validateData ( + array $contentPath, + DataValidator $validator, + mixed $data, + ) : void + { + } } diff --git a/src/Mapping/Field/NumberField.php b/src/Mapping/Field/NumberField.php index 5d7bd80..47a185d 100644 --- a/src/Mapping/Field/NumberField.php +++ b/src/Mapping/Field/NumberField.php @@ -2,16 +2,46 @@ namespace Torr\Storyblok\Mapping\Field; -use Torr\Storyblok\Exception\Mapping\InvalidFieldDefinitionException; +use Symfony\Component\Validator\Constraints\Range; +use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Component\Validator\Constraints\Type; +use Torr\Storyblok\Exception\InvalidFieldConfigurationException; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Validator\DataValidator; #[\Attribute(\Attribute::TARGET_PROPERTY)] final class NumberField extends AbstractField { /** */ - public function __construct (string $key, string $label, mixed $defaultValue = null) + public function __construct ( + string $key, + string $label, + mixed $defaultValue = null, + /** + * A field with 0 decimals will return an int, everything else a float + * + * @type positive-int + */ + private readonly int $numberOfDecimals = 0, + private readonly int|float|null $minValue = null, + private readonly int|float|null $maxValue = null, + + /** + * Only relevant for the UI: defines by how much the value will + * increase/decrease when clicking the step arrows + */ + private readonly int $numberOfSteps = 0, + ) { + if ($this->numberOfDecimals < 0) + { + throw new InvalidFieldConfigurationException(\sprintf( + "numberOfDecimals must be a positive integer, but is %d", + $this->numberOfDecimals, + )); + } + parent::__construct( FieldType::Number, $key, @@ -19,4 +49,50 @@ public function __construct (string $key, string $label, mixed $defaultValue = n $defaultValue, ); } + + /** + * @inheritDoc + */ + public function generateManagementApiData () : array + { + return \array_replace(parent::generateManagementApiData(), [ + "min_value" => $this->minValue, + "max_value" => $this->maxValue, + "decimals" => $this->numberOfDecimals, + "steps" => $this->numberOfSteps, + ]); + } + + + /** + * @inheritDoc + */ + public function validateData (array $contentPath, DataValidator $validator, mixed $data,) : void + { + $mustBeInt = 0 === $this->numberOfDecimals; + + $constraints = [ + new Type("string"), + new Regex( + $mustBeInt + ? "~^\d+$~" + : "~^\d+(\\.\d+)?$~", + message: $mustBeInt + ? "storyblok.field.number.must-be-integer" + : "storyblok.field.number.must-be-float", + ) + ]; + + if (null !== $this->minValue || null !== $this->maxValue) + { + $constraints[] = new Range( + min: $this->minValue, + max: $this->maxValue, + ); + } + + $validator->ensureDataIsValid($contentPath, $data, $constraints); + } + + } diff --git a/src/Mapping/Field/TextField.php b/src/Mapping/Field/TextField.php index 7293716..c289fbc 100644 --- a/src/Mapping/Field/TextField.php +++ b/src/Mapping/Field/TextField.php @@ -2,7 +2,9 @@ namespace Torr\Storyblok\Mapping\Field; +use Symfony\Component\Validator\Constraints\Type; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Validator\DataValidator; #[\Attribute(\Attribute::TARGET_PROPERTY)] final class TextField extends AbstractField @@ -14,6 +16,7 @@ public function __construct ( private readonly bool $multiline = false, private readonly ?int $maxLength = null, private readonly bool $isRightToLeft = false, + private readonly bool $excludeFromTranslationExport = false, ) { parent::__construct( @@ -26,6 +29,17 @@ public function __construct ( ); } + /** + * @inheritDoc + */ + public function validateData (array $contentPath, DataValidator $validator, mixed $data) : void + { + $validator->ensureDataIsValid($contentPath, $data, [ + new Type("string"), + ]); + } + + /** * @inheritDoc */ @@ -36,6 +50,7 @@ public function generateManagementApiData () : array [ "rtl" => $this->isRightToLeft, "max_length" => $this->maxLength, + "no_translate" => $this->excludeFromTranslationExport, ], ); } diff --git a/src/Mapping/FieldAttribute/FieldAttributeInterface.php b/src/Mapping/FieldAttribute/FieldAttributeInterface.php index fccfdec..5d088d4 100644 --- a/src/Mapping/FieldAttribute/FieldAttributeInterface.php +++ b/src/Mapping/FieldAttribute/FieldAttributeInterface.php @@ -2,6 +2,8 @@ namespace Torr\Storyblok\Mapping\FieldAttribute; +use Symfony\Component\Validator\Constraint; + abstract readonly class FieldAttributeInterface { /** @@ -9,4 +11,14 @@ public function __construct ( public array $managementApiData, ) {} + + /** + * Validates the given data + * + * @return Constraint[] + */ + public function getValidationConstraints () : array + { + return []; + } } diff --git a/src/Mapping/FieldAttribute/WithDescription.php b/src/Mapping/FieldAttribute/WithDescription.php index fbda5ce..676f1db 100644 --- a/src/Mapping/FieldAttribute/WithDescription.php +++ b/src/Mapping/FieldAttribute/WithDescription.php @@ -2,6 +2,7 @@ namespace Torr\Storyblok\Mapping\FieldAttribute; + #[\Attribute(\Attribute::TARGET_PROPERTY)] final readonly class WithDescription extends FieldAttributeInterface { diff --git a/src/Mapping/FieldAttribute/WithValidation.php b/src/Mapping/FieldAttribute/WithValidation.php index f82895f..0d167b9 100644 --- a/src/Mapping/FieldAttribute/WithValidation.php +++ b/src/Mapping/FieldAttribute/WithValidation.php @@ -2,20 +2,36 @@ namespace Torr\Storyblok\Mapping\FieldAttribute; +use Symfony\Component\Validator\Constraints\NotNull; + + #[\Attribute(\Attribute::TARGET_PROPERTY)] final readonly class WithValidation extends FieldAttributeInterface { /** */ public function __construct ( - bool $required = true, - ?string $regexp = null, + public bool $required = true, + public ?string $regexp = null, public bool $allowMissingData = false, ) { parent::__construct([ - "required" => $required, - "regex" => $regexp, + "required" => $this->required, + "regex" => $this->regexp, ]); } + + + /** + * @inheritDoc + */ + public function getValidationConstraints () : array + { + return !$this->required || $this->allowMissingData + ? [] + : [ + new NotNull(), + ]; + } } diff --git a/src/Mapping/Storyblok.php b/src/Mapping/Storyblok.php index 13adfe9..303595e 100644 --- a/src/Mapping/Storyblok.php +++ b/src/Mapping/Storyblok.php @@ -14,5 +14,6 @@ public function __construct ( /** @var array */ public array $tags = [], public ?string $previewField = null, + public string|\BackedEnum|null $group = null, ) {} } diff --git a/src/Story/Story.php b/src/Story/Story.php index 7ba9c1f..f87acbc 100644 --- a/src/Story/Story.php +++ b/src/Story/Story.php @@ -2,29 +2,16 @@ namespace Torr\Storyblok\Story; -use Torr\Storyblok\Component\AbstractComponent; -use Torr\Storyblok\Context\ComponentContext; -use Torr\Storyblok\Visitor\DataVisitorInterface; - -abstract class Story implements StoryInterface +abstract class Story { - protected readonly StoryMetaData $metaData; - protected readonly array $content; - /** */ - final public function __construct ( - array $data, - protected readonly AbstractComponent $rootComponent, - protected readonly ComponentContext $dataContext, + public function __construct ( + private readonly StoryMetaData $metaData, ) - { - $this->content = $data["content"]; - $this->metaData = new StoryMetaData($data, $this->rootComponent::getKey()); - } + {} /** - * @inheritDoc */ final public function getUuid () : string { @@ -32,7 +19,6 @@ final public function getUuid () : string } /** - * @inheritDoc */ final public function getMetaData () : StoryMetaData { @@ -40,51 +26,9 @@ final public function getMetaData () : StoryMetaData } /** - * @inheritDoc */ final public function getFullSlug () : string { return $this->metaData->getFullSlug(); } - - /** - * - */ - public function validate (ComponentContext $context) : void - { - $this->rootComponent->validateData( - $context, - $this->content, - label: $this->metaData->getFullSlug(), - ); - } - - - /** - * Returns the transformed data for a single field - */ - protected function getTransformedFieldData ( - string $fieldName, - ?DataVisitorInterface $dataVisitor = null, - ) : mixed - { - return $this->rootComponent->transformField( - $this->content, - $fieldName, - $this->dataContext, - $dataVisitor, - ); - } - - /** - * - */ - public function __debugInfo () : ?array - { - return [ - // hide implementations of these, as they have huge dependency trees - "\0*\0rootComponent" => \get_class($this->rootComponent), - "\0*\0dataContext" => "...", - ]; - } } diff --git a/src/Story/StoryFactory.php b/src/Story/StoryFactory.php index 6ba4ac9..0163ead 100644 --- a/src/Story/StoryFactory.php +++ b/src/Story/StoryFactory.php @@ -3,12 +3,14 @@ namespace Torr\Storyblok\Story; use Psr\Log\LoggerInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Torr\Storyblok\Context\ComponentContext; +use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; -use Torr\Storyblok\Exception\Story\ComponentWithoutStoryException; use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Exception\Story\StoryHydrationFailed; use Torr\Storyblok\Manager\ComponentManager; +use Torr\Storyblok\Validator\DataValidator; final class StoryFactory { @@ -18,6 +20,8 @@ public function __construct ( private readonly ComponentManager $componentManager, private readonly ComponentContext $storyblokContext, private readonly LoggerInterface $logger, + private readonly PropertyAccessorInterface $accessor, + private readonly DataValidator $dataValidator, ) {} /** @@ -25,7 +29,7 @@ public function __construct ( * * @throws StoryHydrationFailed */ - public function createFromApiData (array $data) : ?Story + public function createFromApiData (array $data) : ?object { $type = $data["content"]["component"] ?? null; @@ -51,30 +55,17 @@ public function createFromApiData (array $data) : ?Story try { - $component = $this->componentManager->getComponent($type); - $storyClass = $component->getStoryClass(); - - if (null === $storyClass) - { - throw new ComponentWithoutStoryException(\sprintf( - "Can't create story for component of type '%s', as no story class was defined.", - $component::getKey(), - )); - } - - if (!\is_a($storyClass, Story::class, true)) - { - throw new StoryHydrationFailed(\sprintf( - "Could not hydrate story of type '%s': story class does not extend %s", - $component::getKey(), - Story::class, - )); - } - - $story = new $storyClass($data, $component, $this->storyblokContext); - $story->validate($this->storyblokContext); - - return $story; + $definition = $this->componentManager->getDefinition($type); + $storyClass = $definition->storyClass; + + $metaData = new StoryMetaData($data, $type); + + return $this->mapDataToFields( + [\sprintf("%s (%s)", $storyClass, $type)], + new $storyClass($metaData), + $definition->fields, + $data["content"], + ); } catch (InvalidDataException $exception) { @@ -99,6 +90,36 @@ public function createFromApiData (array $data) : ?Story } } + + /** + * @param FieldDefinition[] $fields + */ + private function mapDataToFields ( + array $contentPath, + object $story, + array $fields, + array $completeData, + ) : object + { + foreach ($fields as $field) + { + $data = $completeData[$field->field->key] ?? null; + + // validate data + $field->validateData( + $contentPath, + $this->dataValidator, + $data, + ); + + // map data + $this->accessor->setValue($story, $field->property, $data); + } + + return $story; + } + + /** * Checks the content of the story to see, whether the story was saved at least once */ diff --git a/src/TorrStoryblokBundle.php b/src/TorrStoryblokBundle.php index 1f31fd7..1ed774f 100644 --- a/src/TorrStoryblokBundle.php +++ b/src/TorrStoryblokBundle.php @@ -10,6 +10,7 @@ use Torr\Storyblok\DependencyInjection\CollectComponentDefinitionsCompilerPass; use Torr\Storyblok\DependencyInjection\StoryblokBundleConfiguration; use Torr\Storyblok\DependencyInjection\StoryblokBundleExtension; +use Torr\Storyblok\Story\Story; final class TorrStoryblokBundle extends Bundle { diff --git a/src/Validator/DataValidator.php b/src/Validator/DataValidator.php index 4e569c6..41a7384 100644 --- a/src/Validator/DataValidator.php +++ b/src/Validator/DataValidator.php @@ -31,7 +31,6 @@ public function __construct ( */ public function ensureDataIsValid ( array $contentPath, - FieldDefinition|AbstractComponent|null $field, mixed $data, array $constraints, ) : void @@ -57,7 +56,6 @@ public function ensureDataIsValid ( : "n/a", ), $contentPath, - $field, $data, $violations, ); diff --git a/src/translations/validator+intl-icu.en.yaml b/src/translations/validator+intl-icu.en.yaml new file mode 100644 index 0000000..dcb3d2e --- /dev/null +++ b/src/translations/validator+intl-icu.en.yaml @@ -0,0 +1,5 @@ +storyblok: + field: + number: + must-be-integer: "This value must be an integer." + must-be-float: "This value must be a float." From 6ab8cf21a32f3bdfb5fe98d1952643dbd2c61342 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 13:39:36 +0100 Subject: [PATCH 05/30] Use NotBlank instead of NotNull --- src/Mapping/FieldAttribute/WithValidation.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Mapping/FieldAttribute/WithValidation.php b/src/Mapping/FieldAttribute/WithValidation.php index 0d167b9..8ab8b41 100644 --- a/src/Mapping/FieldAttribute/WithValidation.php +++ b/src/Mapping/FieldAttribute/WithValidation.php @@ -2,7 +2,7 @@ namespace Torr\Storyblok\Mapping\FieldAttribute; -use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\NotBlank; #[\Attribute(\Attribute::TARGET_PROPERTY)] @@ -31,7 +31,9 @@ public function getValidationConstraints () : array return !$this->required || $this->allowMissingData ? [] : [ - new NotNull(), + // we need to use NotBlank instead of NotNull here, as Storyblok often + // also returns empty strings for empty fields + new NotBlank(), ]; } } From 0a22a460697397ef628ee0290ba464dadeb19b5a Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 13:44:31 +0100 Subject: [PATCH 06/30] Add better logging for storyblok errors --- src/Validator/DataValidator.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Validator/DataValidator.php b/src/Validator/DataValidator.php index 41a7384..ea4121c 100644 --- a/src/Validator/DataValidator.php +++ b/src/Validator/DataValidator.php @@ -2,6 +2,7 @@ namespace Torr\Storyblok\Validator; +use Psr\Log\LoggerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -16,6 +17,7 @@ class DataValidator { public function __construct ( private readonly ValidatorInterface $validator, + private readonly LoggerInterface $logger, ) {} @@ -47,13 +49,22 @@ public function ensureDataIsValid ( if (\count($violations) > 0) { + $formattedPath = \implode(" → ", $contentPath); + $formattedViolations = $violations instanceof ConstraintViolationList + ? (string) $violations + : "n/a"; + + $this->logger->error("Storyblok: Invalid data found at {path}", [ + "path" => $formattedPath, + "violations" => $formattedViolations, + "data" => $data, + ]); + throw new InvalidDataException( \sprintf( "Invalid data found at '%s':\n%s", - \implode(" → ", $contentPath), - $violations instanceof ConstraintViolationList - ? (string) $violations - : "n/a", + $formattedPath, + $formattedViolations, ), $contentPath, $data, From 2a787b222e81743107998f64c02299964a249172 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 13:45:15 +0100 Subject: [PATCH 07/30] Also validate regex values --- src/Mapping/FieldAttribute/WithValidation.php | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Mapping/FieldAttribute/WithValidation.php b/src/Mapping/FieldAttribute/WithValidation.php index 8ab8b41..c71de86 100644 --- a/src/Mapping/FieldAttribute/WithValidation.php +++ b/src/Mapping/FieldAttribute/WithValidation.php @@ -3,6 +3,7 @@ namespace Torr\Storyblok\Mapping\FieldAttribute; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; #[\Attribute(\Attribute::TARGET_PROPERTY)] @@ -28,12 +29,20 @@ public function __construct ( */ public function getValidationConstraints () : array { - return !$this->required || $this->allowMissingData - ? [] - : [ - // we need to use NotBlank instead of NotNull here, as Storyblok often - // also returns empty strings for empty fields - new NotBlank(), - ]; + $constraints = []; + + if ($this->required && !$this->allowMissingData) + { + // we need to use NotBlank instead of NotNull here, as Storyblok often + // also returns empty strings for empty fields + $constraints[] = new NotBlank(); + } + + if (null !== $this->regexp) + { + $constraints[] = new Regex("~" . $this->regexp . "~"); + } + + return $constraints; } } From c4ba6ccc23e6e5d3f1f69059ac7c844faee5bf17 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 13:48:16 +0100 Subject: [PATCH 08/30] Make datatransformer static --- src/Field/Definition/AssetField.php | 13 +++++++------ src/Field/Definition/ChoiceField.php | 7 ++++--- src/Field/Definition/DateTimeField.php | 3 ++- src/Field/Definition/LinkField.php | 11 ++++++----- src/Field/Definition/MarkdownField.php | 3 ++- src/Transformer/DataTransformer.php | 21 +++------------------ 6 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src/Field/Definition/AssetField.php b/src/Field/Definition/AssetField.php index 22affcc..eff3b05 100644 --- a/src/Field/Definition/AssetField.php +++ b/src/Field/Definition/AssetField.php @@ -10,6 +10,7 @@ use Torr\Storyblok\Field\Asset\AssetFileType; use Torr\Storyblok\Field\Data\AssetData; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Transformer\DataTransformer; use Torr\Storyblok\Visitor\DataVisitorInterface; /** @@ -157,12 +158,12 @@ public function transformData ( $transformed = new AssetData( url: $assetUrl, id: $data["id"], - alt: $context->normalizeOptionalString($data["alt"] ?? null), - name: $context->normalizeOptionalString($data["name"] ?? null), - focus: $context->normalizeOptionalString($data["focus"] ?? null), - title: $context->normalizeOptionalString($data["title"] ?? null), - source: $context->normalizeOptionalString($data["source"] ?? null), - copyright: $context->normalizeOptionalString($data["copyright"] ?? null), + alt: DataTransformer::normalizeOptionalString($data["alt"] ?? null), + name: DataTransformer::normalizeOptionalString($data["name"] ?? null), + focus: DataTransformer::normalizeOptionalString($data["focus"] ?? null), + title: DataTransformer::normalizeOptionalString($data["title"] ?? null), + source: DataTransformer::normalizeOptionalString($data["source"] ?? null), + copyright: DataTransformer::normalizeOptionalString($data["copyright"] ?? null), isExternal: $data["is_external_url"] ?? false, width: $width, height: $height, diff --git a/src/Field/Definition/ChoiceField.php b/src/Field/Definition/ChoiceField.php index ad11c07..ff80d24 100644 --- a/src/Field/Definition/ChoiceField.php +++ b/src/Field/Definition/ChoiceField.php @@ -12,6 +12,7 @@ use Torr\Storyblok\Exception\InvalidFieldConfigurationException; use Torr\Storyblok\Field\Choices\ChoicesInterface; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Transformer\DataTransformer; use Torr\Storyblok\Visitor\DataVisitorInterface; final class ChoiceField extends AbstractField @@ -125,7 +126,7 @@ private function validateSingleSelect (ComponentContext $context, array $content \assert(null === $data || \is_string($data) || \is_int($data)); - $data = $context->normalizeOptionalString((string) $data); + $data = DataTransformer::normalizeOptionalString((string) $data); $context->ensureDataIsValid( $contentPath, @@ -180,7 +181,7 @@ private function validateMultiSelect (ComponentContext $context, array $contentP \assert(null === $data || \is_array($data)); $data = \array_map( - static fn (mixed $value) => $context->normalizeOptionalString((string) $value), + static fn (mixed $value) => DataTransformer::normalizeOptionalString((string) $value), // we are in a multiselect, so we expect an array $data ?? [], ); @@ -220,7 +221,7 @@ public function transformData ( if (\is_string($data)) { - $data = $context->normalizeOptionalString($data); + $data = DataTransformer::normalizeOptionalString($data); } $transformed = null !== $data diff --git a/src/Field/Definition/DateTimeField.php b/src/Field/Definition/DateTimeField.php index 2d00528..4b54fbe 100644 --- a/src/Field/Definition/DateTimeField.php +++ b/src/Field/Definition/DateTimeField.php @@ -7,6 +7,7 @@ use Symfony\Component\Validator\Constraints\Type; use Torr\Storyblok\Context\ComponentContext; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Transformer\DataTransformer; use Torr\Storyblok\Visitor\DataVisitorInterface; final class DateTimeField extends AbstractField @@ -76,7 +77,7 @@ public function transformData ( \assert(null === $data || \is_string($data)); // empty fields are also passed as "" - $data = $context->normalizeOptionalString($data); + $data = DataTransformer::normalizeOptionalString($data); $transformed = null !== $data ? \DateTimeImmutable::createFromFormat(self::DATE_TIME_FORMAT, $data) diff --git a/src/Field/Definition/LinkField.php b/src/Field/Definition/LinkField.php index f0dd57d..f35028d 100644 --- a/src/Field/Definition/LinkField.php +++ b/src/Field/Definition/LinkField.php @@ -15,6 +15,7 @@ use Torr\Storyblok\Field\Data\StoryLinkData; use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Manager\Sync\Filter\ResolvableComponentFilter; +use Torr\Storyblok\Transformer\DataTransformer; use Torr\Storyblok\Visitor\DataVisitorInterface; final class LinkField extends AbstractField @@ -150,18 +151,18 @@ private function transformDataToLink ( { if ("story" === $data["linktype"]) { - $id = $context->normalizeOptionalString($data["id"]); + $id = DataTransformer::normalizeOptionalString($data["id"]); // we have the cached_url in the data here, but we can't rely on it, as it might be out of date return new StoryLinkData( id: $id, - anchor: $context->normalizeOptionalString($data["anchor"] ?? null), + anchor: DataTransformer::normalizeOptionalString($data["anchor"] ?? null), ); } if ("email" === $data["linktype"]) { - $email = $context->normalizeOptionalString($data["email"]); + $email = DataTransformer::normalizeOptionalString($data["email"]); return null !== $email ? new EmailLinkData($email) @@ -170,7 +171,7 @@ private function transformDataToLink ( if ("asset" === $data["linktype"]) { - $url = $context->normalizeOptionalString($data["url"]); + $url = DataTransformer::normalizeOptionalString($data["url"]); if (null === $url) { @@ -182,7 +183,7 @@ private function transformDataToLink ( } // "url" === $data["linktype"] - $url = $context->normalizeOptionalString($data["url"]); + $url = DataTransformer::normalizeOptionalString($data["url"]); return null !== $url ? new ExternalLinkData($url) diff --git a/src/Field/Definition/MarkdownField.php b/src/Field/Definition/MarkdownField.php index e32e25b..b0c99fd 100644 --- a/src/Field/Definition/MarkdownField.php +++ b/src/Field/Definition/MarkdownField.php @@ -6,6 +6,7 @@ use Symfony\Component\Validator\Constraints\Type; use Torr\Storyblok\Context\ComponentContext; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Transformer\DataTransformer; use Torr\Storyblok\Visitor\DataVisitorInterface; final class MarkdownField extends AbstractField @@ -75,7 +76,7 @@ public function transformData ( { \assert(null === $data || \is_string($data)); - $transformed = $context->dataTransformer->normalizeOptionalString($data); + $transformed = DataTransformer::normalizeOptionalString($data); $dataVisitor?->onDataVisit($this, $transformed); return $transformed; diff --git a/src/Transformer/DataTransformer.php b/src/Transformer/DataTransformer.php index add078c..71dbf7c 100644 --- a/src/Transformer/DataTransformer.php +++ b/src/Transformer/DataTransformer.php @@ -5,25 +5,10 @@ final class DataTransformer { /** - * @template T - * - * @param T $data The transformed data of the component - * - * @returns T|null + * @param string|null $value + * @return string|null */ - public function transformValue ( - mixed $data, - mixed $component, - ) : mixed - { - // @todo add real implementation - return $data; - } - - /** - * - */ - public function normalizeOptionalString (?string $value) : ?string + public static function normalizeOptionalString (?string $value) : ?string { if (null === $value) { From 11666cd5d34e72f2e92356d30ff93a9ef1b7076e Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Fri, 3 Nov 2023 13:51:59 +0100 Subject: [PATCH 09/30] Add value normalization --- src/Mapping/Field/NumberField.php | 17 +++++++++++++++++ src/Mapping/Field/TextField.php | 12 ++++++++++++ src/Story/StoryFactory.php | 6 +++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Mapping/Field/NumberField.php b/src/Mapping/Field/NumberField.php index 47a185d..28b8d45 100644 --- a/src/Mapping/Field/NumberField.php +++ b/src/Mapping/Field/NumberField.php @@ -63,6 +63,23 @@ public function generateManagementApiData () : array ]); } + /** + * @inheritDoc + */ + public function normalizeData (mixed $data) : int|float|null + { + if (null === $data) + { + return null; + } + + \assert(\is_string($data)); + + return 0 === $this->numberOfDecimals + ? (int) $data + : (float) $data; + } + /** * @inheritDoc diff --git a/src/Mapping/Field/TextField.php b/src/Mapping/Field/TextField.php index c289fbc..e29e80a 100644 --- a/src/Mapping/Field/TextField.php +++ b/src/Mapping/Field/TextField.php @@ -4,6 +4,7 @@ use Symfony\Component\Validator\Constraints\Type; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Transformer\DataTransformer; use Torr\Storyblok\Validator\DataValidator; #[\Attribute(\Attribute::TARGET_PROPERTY)] @@ -29,6 +30,17 @@ public function __construct ( ); } + /** + * @inheritDoc + */ + public function normalizeData (mixed $data) : ?string + { + \assert(null === $data || \is_string($data)); + + return DataTransformer::normalizeOptionalString($data); + } + + /** * @inheritDoc */ diff --git a/src/Story/StoryFactory.php b/src/Story/StoryFactory.php index 0163ead..bdc3b50 100644 --- a/src/Story/StoryFactory.php +++ b/src/Story/StoryFactory.php @@ -113,7 +113,11 @@ private function mapDataToFields ( ); // map data - $this->accessor->setValue($story, $field->property, $data); + $this->accessor->setValue( + $story, + $field->property, + $field->field->normalizeData($data), + ); } return $story; From c78c2b2154fd88f5771b353098295cc87604f9f8 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 11:32:50 +0100 Subject: [PATCH 10/30] Start implementation of embedded fields --- .../Component/ComponentDefinition.php | 31 +++++++++-- .../Component/ComponentDefinitionFactory.php | 53 +++++++++++++++++- .../Field/EmbeddedFieldDefinition.php | 55 +++++++++++++++++++ src/Mapping/Embed/EmbeddedStory.php | 12 ++++ 4 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/Definition/Field/EmbeddedFieldDefinition.php create mode 100644 src/Mapping/Embed/EmbeddedStory.php diff --git a/src/Definition/Component/ComponentDefinition.php b/src/Definition/Component/ComponentDefinition.php index f6045e9..3aa092c 100644 --- a/src/Definition/Component/ComponentDefinition.php +++ b/src/Definition/Component/ComponentDefinition.php @@ -2,6 +2,7 @@ namespace Torr\Storyblok\Definition\Component; +use Torr\Storyblok\Definition\Field\EmbeddedFieldDefinition; use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Mapping\Storyblok; @@ -15,7 +16,7 @@ public function __construct ( public Storyblok $definition, /** @type class-string */ public string $storyClass, - /** @type array */ + /** @type array */ public array $fields, ) { @@ -46,19 +47,39 @@ public function getKey () : string public function generateManagementApiData () : array { - $fields = []; + $fieldSchemas = []; + $position = 0; foreach ($this->fields as $field) { - $fieldData = $field->generateManagementApiData(); - $fields[$field->field->key] = $fieldData; + $additionalFields = $field instanceof FieldDefinition + ? [ + $field->field->key => $field->generateManagementApiData(), + ] + : $field->generateManagementApiDataForAllFields(); + + foreach ($additionalFields as $key => $fieldData) + { + if (\array_key_exists($key, $fieldSchemas)) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Found multiple definitions for field name '%s' in type '%s'", + $key, + $this->storyClass, + )); + } + + // consistently set the position for all fields + $fieldData["pos"] = ++$position; + $fieldSchemas[$key] = $fieldData; + } } return [ "name" => $this->definition->key, "real_name" => $this->definition->key, "display_name" => $this->definition->name, - "schema" => $fields, + "schema" => $fieldSchemas, "preview_field" => $this->definition->previewField, ...$this->definition->type->generateManagementApiData(), ]; diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php index d1a8141..24fcb5a 100644 --- a/src/Definition/Component/ComponentDefinitionFactory.php +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -3,8 +3,10 @@ namespace Torr\Storyblok\Definition\Component; use Torr\Storyblok\Definition\Component\Reflection\ReflectionHelper; +use Torr\Storyblok\Definition\Field\EmbeddedFieldDefinition; use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; +use Torr\Storyblok\Mapping\Embed\EmbeddedStory; use Torr\Storyblok\Mapping\Field\AbstractField; use Torr\Storyblok\Mapping\FieldAttribute\FieldAttributeInterface; use Torr\Storyblok\Mapping\Storyblok; @@ -67,7 +69,7 @@ private function createDefinition ( } /** - * @return array + * @return array */ private function createFieldDefinitions (\ReflectionClass $class) : array { @@ -80,6 +82,21 @@ private function createFieldDefinitions (\ReflectionClass $class) : array continue; } + // check for embeds + $embed = $this->helper->getOptionalSingleAttribute($reflectionProperty, EmbeddedStory::class); + + if (null !== $embed) + { + $embedClass = $this->getSingleClassType($reflectionProperty); + $definitions[$embed->prefix] = new EmbeddedFieldDefinition( + definition: $embed, + property: $reflectionProperty->getName(), + embedClass: $embedClass->getName(), + fields: $this->createFieldDefinitions($embedClass), + ); + continue; + } + $fieldDefinition = $this->helper->getOptionalSingleAttribute($reflectionProperty, AbstractField::class); if (null === $fieldDefinition) @@ -96,4 +113,38 @@ private function createFieldDefinitions (\ReflectionClass $class) : array return $definitions; } + + /** + */ + private function getSingleClassType (\ReflectionProperty $property) : \ReflectionClass + { + $type = $property->getType(); + $fqcn = $property->getDeclaringClass()->getName() . "::" . $property->getName(); + + if (!$type instanceof \ReflectionNamedType) + { + throw new InvalidComponentDefinitionException( + message: \sprintf( + "Can't use non-singular object type on embedded field: %s", + $fqcn, + ), + ); + } + + try + { + return new \ReflectionClass($type->getName()); + } + catch (\ReflectionException $exception) + { + throw new InvalidComponentDefinitionException( + message: \sprintf( + "Invalid type for embedded field '%s': %s", + $fqcn, + $exception->getMessage(), + ), + previous: $exception, + ); + } + } } diff --git a/src/Definition/Field/EmbeddedFieldDefinition.php b/src/Definition/Field/EmbeddedFieldDefinition.php new file mode 100644 index 0000000..f983062 --- /dev/null +++ b/src/Definition/Field/EmbeddedFieldDefinition.php @@ -0,0 +1,55 @@ + */ + public array $fields, + ) {} + + + /** + * + */ + public function generateManagementApiDataForAllFields () : array + { + $keys = []; + $schemas = []; + $prefix = \rtrim($this->definition->prefix, "_") . "_"; + + foreach ($this->fields as $field) + { + $fullKey = $prefix . $field->field->key; + + if (\array_key_exists($fullKey, $schemas)) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Found multiple definitions for field name '%s' in embed '%s'", + $fullKey, + $this->embedClass, + )); + } + + $schemas[$fullKey] = $field->generateManagementApiData(); + $keys[] = $fullKey; + } + + + $schemas[$prefix . "_embed"] = [ + "type" => FieldType::Section->value, + "display_name" => $this->definition->label, + "keys" => $keys, + ]; + + return $schemas; + } +} diff --git a/src/Mapping/Embed/EmbeddedStory.php b/src/Mapping/Embed/EmbeddedStory.php new file mode 100644 index 0000000..f09eef0 --- /dev/null +++ b/src/Mapping/Embed/EmbeddedStory.php @@ -0,0 +1,12 @@ + Date: Mon, 6 Nov 2023 12:12:40 +0100 Subject: [PATCH 11/30] Rename factory to hydrator --- src/Api/ContentApi.php | 4 ++-- src/Story/{StoryFactory.php => StoryHydrator.php} | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) rename src/Story/{StoryFactory.php => StoryHydrator.php} (96%) diff --git a/src/Api/ContentApi.php b/src/Api/ContentApi.php index 93e9440..12edebb 100644 --- a/src/Api/ContentApi.php +++ b/src/Api/ContentApi.php @@ -17,7 +17,7 @@ use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Release\ReleaseVersion; use Torr\Storyblok\Story\Story; -use Torr\Storyblok\Story\StoryFactory; +use Torr\Storyblok\Story\StoryHydrator; final class ContentApi implements ResetInterface { @@ -31,7 +31,7 @@ final class ContentApi implements ResetInterface public function __construct ( HttpClientInterface $client, private readonly StoryblokConfig $config, - private readonly StoryFactory $storyFactory, + private readonly StoryHydrator $storyFactory, private readonly ComponentManager $componentManager, private readonly LoggerInterface $logger, ) diff --git a/src/Story/StoryFactory.php b/src/Story/StoryHydrator.php similarity index 96% rename from src/Story/StoryFactory.php rename to src/Story/StoryHydrator.php index bdc3b50..b8c0cfd 100644 --- a/src/Story/StoryFactory.php +++ b/src/Story/StoryHydrator.php @@ -4,7 +4,6 @@ use Psr\Log\LoggerInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Torr\Storyblok\Context\ComponentContext; use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; use Torr\Storyblok\Exception\Story\InvalidDataException; @@ -12,13 +11,12 @@ use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Validator\DataValidator; -final class StoryFactory +final class StoryHydrator { /** */ public function __construct ( private readonly ComponentManager $componentManager, - private readonly ComponentContext $storyblokContext, private readonly LoggerInterface $logger, private readonly PropertyAccessorInterface $accessor, private readonly DataValidator $dataValidator, From e9b1789d9421366bdce993b2cc9acfceae556e39 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 13:28:33 +0100 Subject: [PATCH 12/30] Finalize embed handling --- .../Component/ComponentDefinitionFactory.php | 33 +++++++++++--- .../Field/EmbeddedFieldDefinition.php | 43 +++++++++++-------- src/Mapping/Embed/EmbeddedStory.php | 15 +++++-- src/Story/StoryHydrator.php | 39 ++++++++++++----- 4 files changed, 91 insertions(+), 39 deletions(-) diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php index 24fcb5a..cda855b 100644 --- a/src/Definition/Component/ComponentDefinitionFactory.php +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -71,7 +71,10 @@ private function createDefinition ( /** * @return array */ - private function createFieldDefinitions (\ReflectionClass $class) : array + private function createFieldDefinitions ( + \ReflectionClass $class, + bool $allowEmbeds = true, + ) : array { $definitions = []; @@ -87,12 +90,20 @@ private function createFieldDefinitions (\ReflectionClass $class) : array if (null !== $embed) { + if (!$allowEmbeds) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Can't use embedded field in embedded type at '%s'", + $this->formatPropertyName($reflectionProperty), + )); + } + $embedClass = $this->getSingleClassType($reflectionProperty); $definitions[$embed->prefix] = new EmbeddedFieldDefinition( definition: $embed, property: $reflectionProperty->getName(), embedClass: $embedClass->getName(), - fields: $this->createFieldDefinitions($embedClass), + fields: $this->createFieldDefinitions($embedClass, allowEmbeds: false), ); continue; } @@ -119,14 +130,13 @@ private function createFieldDefinitions (\ReflectionClass $class) : array private function getSingleClassType (\ReflectionProperty $property) : \ReflectionClass { $type = $property->getType(); - $fqcn = $property->getDeclaringClass()->getName() . "::" . $property->getName(); if (!$type instanceof \ReflectionNamedType) { throw new InvalidComponentDefinitionException( message: \sprintf( "Can't use non-singular object type on embedded field: %s", - $fqcn, + $this->formatPropertyName($property), ), ); } @@ -140,11 +150,24 @@ private function getSingleClassType (\ReflectionProperty $property) : \Reflectio throw new InvalidComponentDefinitionException( message: \sprintf( "Invalid type for embedded field '%s': %s", - $fqcn, + $this->formatPropertyName($property), $exception->getMessage(), ), previous: $exception, ); } } + + + /** + * + */ + private function formatPropertyName (\ReflectionProperty $property) : string + { + return \sprintf( + "%s::$%s", + $property->getDeclaringClass()->getName(), + $property->getName(), + ); + } } diff --git a/src/Definition/Field/EmbeddedFieldDefinition.php b/src/Definition/Field/EmbeddedFieldDefinition.php index f983062..9fdbd23 100644 --- a/src/Definition/Field/EmbeddedFieldDefinition.php +++ b/src/Definition/Field/EmbeddedFieldDefinition.php @@ -8,13 +8,31 @@ final readonly class EmbeddedFieldDefinition { + /** + * @type array + */ + public array $fields; + + /** + * @param array $fields + */ public function __construct ( public EmbeddedStory $definition, public string $property, + /** @var class-string */ public string $embedClass, - /** @type array */ - public array $fields, - ) {} + array $fields, + ) + { + $transformed = []; + + foreach ($fields as $key => $field) + { + $transformed[$this->definition->prefix . $key] = $field; + } + + $this->fields = $transformed; + } /** @@ -22,32 +40,19 @@ public function __construct ( */ public function generateManagementApiDataForAllFields () : array { - $keys = []; $schemas = []; $prefix = \rtrim($this->definition->prefix, "_") . "_"; - foreach ($this->fields as $field) + foreach ($this->fields as $key => $field) { - $fullKey = $prefix . $field->field->key; - - if (\array_key_exists($fullKey, $schemas)) - { - throw new InvalidComponentDefinitionException(\sprintf( - "Found multiple definitions for field name '%s' in embed '%s'", - $fullKey, - $this->embedClass, - )); - } - - $schemas[$fullKey] = $field->generateManagementApiData(); - $keys[] = $fullKey; + $schemas[$key] = $field->generateManagementApiData(); } $schemas[$prefix . "_embed"] = [ "type" => FieldType::Section->value, "display_name" => $this->definition->label, - "keys" => $keys, + "keys" => \array_keys($this->fields), ]; return $schemas; diff --git a/src/Mapping/Embed/EmbeddedStory.php b/src/Mapping/Embed/EmbeddedStory.php index f09eef0..3e262f7 100644 --- a/src/Mapping/Embed/EmbeddedStory.php +++ b/src/Mapping/Embed/EmbeddedStory.php @@ -3,10 +3,17 @@ namespace Torr\Storyblok\Mapping\Embed; #[\Attribute(\Attribute::TARGET_PROPERTY)] -final class EmbeddedStory +final readonly class EmbeddedStory { + public string $prefix; + + /** + */ public function __construct ( - public readonly string $prefix, - public readonly string $label, - ) {} + string $prefix, + public string $label, + ) + { + $this->prefix = \rtrim($prefix, "_") . "_"; + } } diff --git a/src/Story/StoryHydrator.php b/src/Story/StoryHydrator.php index b8c0cfd..df0ea6c 100644 --- a/src/Story/StoryHydrator.php +++ b/src/Story/StoryHydrator.php @@ -4,11 +4,13 @@ use Psr\Log\LoggerInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Torr\Storyblok\Definition\Field\EmbeddedFieldDefinition; use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Exception\Story\StoryHydrationFailed; use Torr\Storyblok\Manager\ComponentManager; +use Torr\Storyblok\Mapping\Embed\EmbeddedStory; use Torr\Storyblok\Validator\DataValidator; final class StoryHydrator @@ -90,7 +92,7 @@ public function createFromApiData (array $data) : ?object /** - * @param FieldDefinition[] $fields + * @param array $fields */ private function mapDataToFields ( array $contentPath, @@ -99,22 +101,37 @@ private function mapDataToFields ( array $completeData, ) : object { - foreach ($fields as $field) + foreach ($fields as $fieldKey => $field) { - $data = $completeData[$field->field->key] ?? null; - - // validate data - $field->validateData( - $contentPath, - $this->dataValidator, - $data, - ); + if ($field instanceof EmbeddedFieldDefinition) + { + $embed = new ($field->embedClass)(); + $transformed = $this->mapDataToFields( + [...$contentPath, $field->property], + $embed, + $field->fields, + $completeData, + ); + } + else + { + $data = $completeData[$fieldKey] ?? null; + + // validate data + $field->validateData( + $contentPath, + $this->dataValidator, + $data, + ); + + $transformed = $field->field->normalizeData($data); + } // map data $this->accessor->setValue( $story, $field->property, - $field->field->normalizeData($data), + $transformed, ); } From a4c9daaa9a4041b3c754d569679f92b9cb4cc4dc Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 13:57:12 +0100 Subject: [PATCH 13/30] Improve name of transform method --- src/Mapping/Field/AbstractField.php | 4 ++-- src/Mapping/Field/NumberField.php | 2 +- src/Mapping/Field/TextField.php | 2 +- src/Story/StoryHydrator.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mapping/Field/AbstractField.php b/src/Mapping/Field/AbstractField.php index 43a13c4..82db0be 100644 --- a/src/Mapping/Field/AbstractField.php +++ b/src/Mapping/Field/AbstractField.php @@ -28,9 +28,9 @@ public function generateManagementApiData () : array } /** - * Normalizes the data from Storyblok to be more strict + * Transforms the raw data from Storyblok to a sanitized format. */ - public function normalizeData (mixed $data) : mixed + public function transformRawData (mixed $data) : mixed { return $data; } diff --git a/src/Mapping/Field/NumberField.php b/src/Mapping/Field/NumberField.php index 28b8d45..3038d0c 100644 --- a/src/Mapping/Field/NumberField.php +++ b/src/Mapping/Field/NumberField.php @@ -66,7 +66,7 @@ public function generateManagementApiData () : array /** * @inheritDoc */ - public function normalizeData (mixed $data) : int|float|null + public function transformRawData (mixed $data) : int|float|null { if (null === $data) { diff --git a/src/Mapping/Field/TextField.php b/src/Mapping/Field/TextField.php index e29e80a..e865621 100644 --- a/src/Mapping/Field/TextField.php +++ b/src/Mapping/Field/TextField.php @@ -33,7 +33,7 @@ public function __construct ( /** * @inheritDoc */ - public function normalizeData (mixed $data) : ?string + public function transformRawData (mixed $data) : ?string { \assert(null === $data || \is_string($data)); diff --git a/src/Story/StoryHydrator.php b/src/Story/StoryHydrator.php index df0ea6c..92d5dee 100644 --- a/src/Story/StoryHydrator.php +++ b/src/Story/StoryHydrator.php @@ -124,7 +124,7 @@ private function mapDataToFields ( $data, ); - $transformed = $field->field->normalizeData($data); + $transformed = $field->field->transformRawData($data); } // map data From e7884bb5fc36c5ab77584c732397be4365d908ef Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 13:57:38 +0100 Subject: [PATCH 14/30] Stories should implement the interface --- src/Story/Story.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Story/Story.php b/src/Story/Story.php index f87acbc..47761ab 100644 --- a/src/Story/Story.php +++ b/src/Story/Story.php @@ -2,7 +2,7 @@ namespace Torr\Storyblok\Story; -abstract class Story +abstract class Story implements StoryInterface { /** */ From 4058f7acc0593428559d24d0cc358b322d69d061 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 14:42:54 +0100 Subject: [PATCH 15/30] Finaliz blok mapping + hydration --- composer.json | 2 +- src/Api/ContentApi.php | 18 +-- src/Api/Data/PaginatedApiResult.php | 4 +- src/Command/DebugCommand.php | 3 +- src/Component/AbstractComponent.php | 4 +- .../Component/ComponentDefinition.php | 28 ++-- .../Component/ComponentDefinitionFactory.php | 18 ++- ...ollectComponentDefinitionsCompilerPass.php | 24 ++-- src/{Story => Hydrator}/StoryHydrator.php | 80 +++++++++--- src/Manager/ComponentManager.php | 10 +- src/Mapping/Field/AbstractField.php | 10 +- src/Mapping/Field/BloksField.php | 123 ++++++++++++++++++ src/Mapping/Field/NumberField.php | 3 +- src/Mapping/Field/TextField.php | 3 +- src/Mapping/StoryBlok.php | 41 ++++++ src/Mapping/StoryDocument.php | 41 ++++++ src/Mapping/Storyblok.php | 19 --- src/Story/Blok.php | 24 ++++ src/Story/{Story.php => Document.php} | 8 +- src/Story/Helper/ComponentPreviewData.php | 4 +- src/Story/MetaData/BlokMetaData.php | 46 +++++++ .../DocumentMetaData.php} | 4 +- src/Story/StoryInterface.php | 18 --- src/TorrStoryblokBundle.php | 2 +- 24 files changed, 423 insertions(+), 114 deletions(-) rename src/{Story => Hydrator}/StoryHydrator.php (68%) create mode 100644 src/Mapping/Field/BloksField.php create mode 100644 src/Mapping/StoryBlok.php create mode 100644 src/Mapping/StoryDocument.php delete mode 100644 src/Mapping/Storyblok.php create mode 100644 src/Story/Blok.php rename src/Story/{Story.php => Document.php} (66%) create mode 100644 src/Story/MetaData/BlokMetaData.php rename src/Story/{StoryMetaData.php => MetaData/DocumentMetaData.php} (98%) delete mode 100644 src/Story/StoryInterface.php diff --git a/composer.json b/composer.json index faec276..0298823 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "symfony/lock": "^6.2", "symfony/property-access": "^6.3", "symfony/rate-limiter": "^6.2", - "symfony/string": "^6.2", + "symfony/string": "^6.3", "symfony/validator": "^6.1" }, "require-dev": { diff --git a/src/Api/ContentApi.php b/src/Api/ContentApi.php index 12edebb..57fe2be 100644 --- a/src/Api/ContentApi.php +++ b/src/Api/ContentApi.php @@ -16,8 +16,8 @@ use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Release\ReleaseVersion; -use Torr\Storyblok\Story\Story; -use Torr\Storyblok\Story\StoryHydrator; +use Torr\Storyblok\Story\Document; +use Torr\Storyblok\Hydrator\StoryHydrator; final class ContentApi implements ResetInterface { @@ -54,7 +54,7 @@ public function __construct ( public function fetchSingleStory ( string|int $identifier, ReleaseVersion $version = ReleaseVersion::PUBLISHED, - ) : ?Story + ) : ?Document { try { @@ -102,14 +102,14 @@ public function fetchSingleStory ( * This method provides certain commonly used named parameters, but also supports passing arbitrary parameters * in the parameter. Passing named parameters will always overwrite parameters in $query. * - * @template TStory of Story + * @template TStory of Document * * @param class-string $storyType * - * @throws ContentRequestFailedException - * @throws UnknownStoryTypeException - * * @return array + *@throws UnknownStoryTypeException + * + * @throws ContentRequestFailedException */ public function fetchStories ( string $storyType, @@ -154,9 +154,9 @@ public function fetchStories ( * * @param string|string[]|null $slug * - * @throws ContentRequestFailedException + * @return array + *@throws ContentRequestFailedException * - * @return array */ public function fetchAllStories ( string|array|null $slug, diff --git a/src/Api/Data/PaginatedApiResult.php b/src/Api/Data/PaginatedApiResult.php index 551e86b..0884b25 100644 --- a/src/Api/Data/PaginatedApiResult.php +++ b/src/Api/Data/PaginatedApiResult.php @@ -2,12 +2,12 @@ namespace Torr\Storyblok\Api\Data; -use Torr\Storyblok\Story\Story; +use Torr\Storyblok\Story\Document; final class PaginatedApiResult { /** - * @param array $stories + * @param array $stories */ public function __construct ( public readonly int $perPage, diff --git a/src/Command/DebugCommand.php b/src/Command/DebugCommand.php index 5d57235..e88b69f 100644 --- a/src/Command/DebugCommand.php +++ b/src/Command/DebugCommand.php @@ -9,6 +9,7 @@ use Torr\Cli\Console\Style\TorrStyle; use Torr\Storyblok\Api\ContentApi; use Torr\Storyblok\Manager\ComponentManager; +use Torr\Storyblok\Release\ReleaseVersion; #[AsCommand("storyblok:debug")] final class DebugCommand extends Command @@ -45,7 +46,7 @@ protected function execute (InputInterface $input, OutputInterface $output) : in if ($input->getOption("fetch")) { - $stories = $this->contentApi->fetchAllStories(null); + $stories = $this->contentApi->fetchAllStories(null, version: ReleaseVersion::DRAFT); dd($stories); } diff --git a/src/Component/AbstractComponent.php b/src/Component/AbstractComponent.php index 55c0a48..13e9370 100644 --- a/src/Component/AbstractComponent.php +++ b/src/Component/AbstractComponent.php @@ -15,14 +15,14 @@ use Torr\Storyblok\Field\Data\Helper\InlinedTransformedData; use Torr\Storyblok\Field\FieldDefinition; use Torr\Storyblok\Management\ManagementApiData; -use Torr\Storyblok\Story\Story; +use Torr\Storyblok\Story\Document; use Torr\Storyblok\Visitor\ComponentDataVisitorInterface; use Torr\Storyblok\Visitor\DataVisitorInterface; /** * Base class for all components registered in the system * - * @template TStory of Story + * @template TStory of Document */ abstract class AbstractComponent { diff --git a/src/Definition/Component/ComponentDefinition.php b/src/Definition/Component/ComponentDefinition.php index 3aa092c..fc2a470 100644 --- a/src/Definition/Component/ComponentDefinition.php +++ b/src/Definition/Component/ComponentDefinition.php @@ -5,16 +5,18 @@ use Torr\Storyblok\Definition\Field\EmbeddedFieldDefinition; use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; -use Torr\Storyblok\Mapping\Storyblok; -use Torr\Storyblok\Story\Story; +use Torr\Storyblok\Mapping\StoryBlok; +use Torr\Storyblok\Mapping\StoryDocument; +use Torr\Storyblok\Story\Document; +use function Symfony\Component\String\u; final readonly class ComponentDefinition { /** */ public function __construct ( - public Storyblok $definition, - /** @type class-string */ + public StoryDocument|StoryBlok $definition, + /** @type class-string */ public string $storyClass, /** @type array */ public array $fields, @@ -32,16 +34,12 @@ public function __construct ( /** */ - public function getDisplayName () : string + public function getName () : string { - return $this->definition->name; - } - - /** - */ - public function getKey () : string - { - return $this->definition->key; + return $this->definition->name + ?? u($this->definition->key) + ->title() + ->toString(); } @@ -78,10 +76,10 @@ public function generateManagementApiData () : array return [ "name" => $this->definition->key, "real_name" => $this->definition->key, - "display_name" => $this->definition->name, + "display_name" => $this->getName(), "schema" => $fieldSchemas, "preview_field" => $this->definition->previewField, - ...$this->definition->type->generateManagementApiData(), + ...$this->definition->getComponentTypeApiData(), ]; } } diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php index cda855b..9db6222 100644 --- a/src/Definition/Component/ComponentDefinitionFactory.php +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -9,7 +9,8 @@ use Torr\Storyblok\Mapping\Embed\EmbeddedStory; use Torr\Storyblok\Mapping\Field\AbstractField; use Torr\Storyblok\Mapping\FieldAttribute\FieldAttributeInterface; -use Torr\Storyblok\Mapping\Storyblok; +use Torr\Storyblok\Mapping\StoryBlok; +use Torr\Storyblok\Mapping\StoryDocument; final readonly class ComponentDefinitionFactory { @@ -48,10 +49,21 @@ private function createDefinition ( try { $reflectionClass = new \ReflectionClass($storyblokClass); - $blok = $this->helper->getRequiredSingleAttribute($reflectionClass, Storyblok::class); + $definition = $this->helper->getOptionalSingleAttribute($reflectionClass, StoryDocument::class) + ?? $this->helper->getOptionalSingleAttribute($reflectionClass, StoryBlok::class); + + if (null === $definition) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Could not find required attribute of type '%s' or '%s' on class '%s'.", + StoryDocument::class, + StoryBlok::class, + $reflectionClass->getName(), + )); + } return new ComponentDefinition( - $blok, + $definition, $storyblokClass, $this->createFieldDefinitions($reflectionClass), ); diff --git a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php index df034dc..e0a5171 100644 --- a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php +++ b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php @@ -7,8 +7,9 @@ use Torr\Storyblok\Exception\Component\DuplicateComponentKeyException; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Manager\ComponentManager; -use Torr\Storyblok\Mapping\Storyblok; -use Torr\Storyblok\Story\Story; +use Torr\Storyblok\Mapping\StoryBlok; +use Torr\Storyblok\Mapping\StoryDocument; +use Torr\Storyblok\Story\Document; final class CollectComponentDefinitionsCompilerPass implements CompilerPassInterface { @@ -36,19 +37,19 @@ public function process (ContainerBuilder $container) : void continue; } - if (!\is_a($class, Story::class, true)) + if (!\is_a($class, $attribute->getRequiredExtendedClass(), true)) { throw new InvalidComponentDefinitionException(\sprintf( - "Story '%s' must extend %s.", + "Storyblok element '%s' must extend %s.", $class, - Story::class, + $attribute->getRequiredExtendedClass(), )); } if (\array_key_exists($attribute->key, $definitions)) { throw new DuplicateComponentKeyException(\sprintf( - "Found multiple story definitions for key '%s'. One in '%s' and one in '%s'", + "Found multiple storyblok definitions for key '%s'. One in '%s' and one in '%s'", $attribute->key, $definitions[$attribute->key], $definition->getClass(), @@ -69,12 +70,14 @@ public function process (ContainerBuilder $container) : void * * @param class-string $class */ - private function extractKey (string $class) : ?Storyblok + private function extractKey (string $class) : StoryDocument|StoryBlok|null { try { $reflectionClass = new \ReflectionClass($class); - $reflectionAttribute = $reflectionClass->getAttributes(Storyblok::class)[0] ?? null; + $reflectionAttribute = $reflectionClass->getAttributes(StoryDocument::class)[0] + ?? $reflectionClass->getAttributes(StoryBlok::class)[0] + ?? null; if (null === $reflectionAttribute) { @@ -82,10 +85,11 @@ private function extractKey (string $class) : ?Storyblok } $attribute = $reflectionAttribute->newInstance(); - \assert($attribute instanceof Storyblok); + \assert($attribute instanceof StoryDocument || $attribute instanceof StoryBlok); + return $attribute; } - catch (\ReflectionException $e) + catch (\ReflectionException) { return null; } diff --git a/src/Story/StoryHydrator.php b/src/Hydrator/StoryHydrator.php similarity index 68% rename from src/Story/StoryHydrator.php rename to src/Hydrator/StoryHydrator.php index 92d5dee..0752e1a 100644 --- a/src/Story/StoryHydrator.php +++ b/src/Hydrator/StoryHydrator.php @@ -1,6 +1,6 @@ componentManager->getDefinition($type); - $storyClass = $definition->storyClass; - - $metaData = new StoryMetaData($data, $type); - - return $this->mapDataToFields( - [\sprintf("%s (%s)", $storyClass, $type)], - new $storyClass($metaData), - $definition->fields, - $data["content"], - ); + return $this->hydrateDocument($type, $data); } catch (InvalidDataException $exception) { @@ -90,13 +85,66 @@ public function createFromApiData (array $data) : ?object } } + /** + * Hydrates the document with the data + */ + public function hydrateDocument ( + string $type, + array $data, + ) : Document + { + $definition = $this->componentManager->getDefinition($type); + $storyClass = $definition->storyClass; + + \assert(\is_a($storyClass, Document::class, true)); + + $metaData = new DocumentMetaData($data, $type); + $document = new $storyClass($metaData); + + return $this->mapDataToFields( + [\sprintf("%s (%s)", $storyClass, $type)], + $document, + $definition->fields, + $data["content"], + ); + } + + /** + * + */ + public function hydrateBlok ( + array $contentPath, + string $type, + array $data, + ) : Blok + { + $definition = $this->componentManager->getDefinition($type); + $blokClass = $definition->storyClass; + + \assert(\is_a($blokClass, Blok::class, true)); + + $metaData = new BlokMetaData($data); + $blok = new $blokClass($metaData); + + return $this->mapDataToFields( + [...$contentPath, \sprintf("%s (%s)", $blokClass, $type)], + $blok, + $definition->fields, + $data, + ); + } + /** + * @template ContentElement of object + * + * @param ContentElement $item * @param array $fields + * @return ContentElement */ private function mapDataToFields ( array $contentPath, - object $story, + object $item, array $fields, array $completeData, ) : object @@ -124,18 +172,18 @@ private function mapDataToFields ( $data, ); - $transformed = $field->field->transformRawData($data); + $transformed = $field->field->transformRawData($contentPath, $data, $this); } // map data $this->accessor->setValue( - $story, + $item, $field->property, $transformed, ); } - return $story; + return $item; } diff --git a/src/Manager/ComponentManager.php b/src/Manager/ComponentManager.php index 9c15e0f..ad2c740 100644 --- a/src/Manager/ComponentManager.php +++ b/src/Manager/ComponentManager.php @@ -13,8 +13,8 @@ use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; use Torr\Storyblok\Exception\Component\UnknownStoryTypeException; -use Torr\Storyblok\Mapping\Storyblok; -use Torr\Storyblok\Story\Story; +use Torr\Storyblok\Mapping\StoryDocument; +use Torr\Storyblok\Story\Document; /** * @final @@ -65,13 +65,13 @@ public function getAllComponents () : array /** * Returns the first component that creates a story of the given type * - * @template TStory of Story + * @template TStory of Document * * @param class-string $storyType * - * @throws UnknownStoryTypeException - * * @return AbstractComponent + *@throws UnknownStoryTypeException + * */ public function getComponentByStoryType (string $storyType) : AbstractComponent { diff --git a/src/Mapping/Field/AbstractField.php b/src/Mapping/Field/AbstractField.php index 82db0be..e3678f4 100644 --- a/src/Mapping/Field/AbstractField.php +++ b/src/Mapping/Field/AbstractField.php @@ -3,6 +3,7 @@ namespace Torr\Storyblok\Mapping\Field; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Hydrator\StoryHydrator; use Torr\Storyblok\Validator\DataValidator; abstract class AbstractField @@ -30,7 +31,11 @@ public function generateManagementApiData () : array /** * Transforms the raw data from Storyblok to a sanitized format. */ - public function transformRawData (mixed $data) : mixed + public function transformRawData ( + array $contentPath, + mixed $data, + StoryHydrator $hydrator, + ) : mixed { return $data; } @@ -44,6 +49,5 @@ public function validateData ( DataValidator $validator, mixed $data, ) : void - { - } + {} } diff --git a/src/Mapping/Field/BloksField.php b/src/Mapping/Field/BloksField.php new file mode 100644 index 0000000..d7c02f5 --- /dev/null +++ b/src/Mapping/Field/BloksField.php @@ -0,0 +1,123 @@ +minimumNumberOfBloks && $this->minimumNumberOfBloks < 0) + { + throw new InvalidFieldConfigurationException(\sprintf( + "The minimum number of blocks (%d) can't be negative", + $this->minimumNumberOfBloks, + )); + } + + if (null !== $this->maximumNumberOfBloks && $this->maximumNumberOfBloks < 0) + { + throw new InvalidFieldConfigurationException(\sprintf( + "The maximum number of blocks (%d) can't be negative", + $this->maximumNumberOfBloks, + )); + } + + if ( + null !== $this->minimumNumberOfBloks + && null !== $this->maximumNumberOfBloks + && $this->minimumNumberOfBloks > $this->maximumNumberOfBloks + ) + { + throw new InvalidFieldConfigurationException(\sprintf( + "The minimum number of blocks (%d) value can't be higher than the maximum (%d)", + $this->minimumNumberOfBloks, + $this->maximumNumberOfBloks, + )); + } + } + + /** + * @inheritDoc + */ + public function validateData (array $contentPath, DataValidator $validator, mixed $data) : void + { + $data ??= []; + + $validator->ensureDataIsValid( + $contentPath, + $data, + [ + new Type("array"), + null !== $this->minimumNumberOfBloks || null !== $this->maximumNumberOfBloks + ? new Count( + min: $this->minimumNumberOfBloks, + max: $this->maximumNumberOfBloks, + ) + : null, + new All( + constraints: [ + new NotNull(), + new Type("array"), + new Collection( + fields: [ + "component" => [ + new NotNull(), + new Type("string"), + ], + ], + allowExtraFields: true, + allowMissingFields: false, + ), + ], + ), + ], + ); + } + + /** + * @inheritDoc + */ + public function transformRawData (array $contentPath, mixed $data, StoryHydrator $hydrator) : array + { + $data ??= []; + \assert(\is_array($data)); + + $result = []; + + foreach ($data as $index => $entry) + { + $type = $entry["component"]; + $result[] = $hydrator->hydrateBlok( + [...$contentPath, \sprintf("Index #%d", $index)], + $type, + $entry, + ); + } + + return $result; + } +} diff --git a/src/Mapping/Field/NumberField.php b/src/Mapping/Field/NumberField.php index 3038d0c..9608729 100644 --- a/src/Mapping/Field/NumberField.php +++ b/src/Mapping/Field/NumberField.php @@ -7,6 +7,7 @@ use Symfony\Component\Validator\Constraints\Type; use Torr\Storyblok\Exception\InvalidFieldConfigurationException; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Hydrator\StoryHydrator; use Torr\Storyblok\Validator\DataValidator; #[\Attribute(\Attribute::TARGET_PROPERTY)] @@ -66,7 +67,7 @@ public function generateManagementApiData () : array /** * @inheritDoc */ - public function transformRawData (mixed $data) : int|float|null + public function transformRawData (array $contentPath, mixed $data, StoryHydrator $hydrator) : int|float|null { if (null === $data) { diff --git a/src/Mapping/Field/TextField.php b/src/Mapping/Field/TextField.php index e865621..1efe8f9 100644 --- a/src/Mapping/Field/TextField.php +++ b/src/Mapping/Field/TextField.php @@ -4,6 +4,7 @@ use Symfony\Component\Validator\Constraints\Type; use Torr\Storyblok\Field\FieldType; +use Torr\Storyblok\Hydrator\StoryHydrator; use Torr\Storyblok\Transformer\DataTransformer; use Torr\Storyblok\Validator\DataValidator; @@ -33,7 +34,7 @@ public function __construct ( /** * @inheritDoc */ - public function transformRawData (mixed $data) : ?string + public function transformRawData (array $contentPath, mixed $data, StoryHydrator $hydrator) : ?string { \assert(null === $data || \is_string($data)); diff --git a/src/Mapping/StoryBlok.php b/src/Mapping/StoryBlok.php new file mode 100644 index 0000000..0a51d08 --- /dev/null +++ b/src/Mapping/StoryBlok.php @@ -0,0 +1,41 @@ + */ + public array $tags = [], + public ?string $previewField = null, + public string|\BackedEnum|null $group = null, + ) {} + + /** + * + */ + public function getComponentTypeApiData () : array + { + return [ + "is_root" => false, + "is_nestable" => true, + ]; + } + + + /** + * @return class-string + */ + public function getRequiredExtendedClass () : string + { + return Blok::class; + } +} diff --git a/src/Mapping/StoryDocument.php b/src/Mapping/StoryDocument.php new file mode 100644 index 0000000..f9be57c --- /dev/null +++ b/src/Mapping/StoryDocument.php @@ -0,0 +1,41 @@ + */ + public array $tags = [], + public ?string $previewField = null, + public string|\BackedEnum|null $group = null, + ) {} + + /** + * + */ + public function getComponentTypeApiData () : array + { + return [ + "is_root" => true, + "is_nestable" => false, + ]; + } + + + /** + * @return class-string + */ + public function getRequiredExtendedClass () : string + { + return Document::class; + } +} diff --git a/src/Mapping/Storyblok.php b/src/Mapping/Storyblok.php deleted file mode 100644 index 303595e..0000000 --- a/src/Mapping/Storyblok.php +++ /dev/null @@ -1,19 +0,0 @@ - */ - public array $tags = [], - public ?string $previewField = null, - public string|\BackedEnum|null $group = null, - ) {} -} diff --git a/src/Story/Blok.php b/src/Story/Blok.php new file mode 100644 index 0000000..aff17b2 --- /dev/null +++ b/src/Story/Blok.php @@ -0,0 +1,24 @@ +metaData; + } +} + diff --git a/src/Story/Story.php b/src/Story/Document.php similarity index 66% rename from src/Story/Story.php rename to src/Story/Document.php index 47761ab..1792fe0 100644 --- a/src/Story/Story.php +++ b/src/Story/Document.php @@ -2,12 +2,14 @@ namespace Torr\Storyblok\Story; -abstract class Story implements StoryInterface +use Torr\Storyblok\Story\MetaData\DocumentMetaData; + +abstract class Document { /** */ public function __construct ( - private readonly StoryMetaData $metaData, + private readonly DocumentMetaData $metaData, ) {} @@ -20,7 +22,7 @@ final public function getUuid () : string /** */ - final public function getMetaData () : StoryMetaData + final public function getMetaData () : DocumentMetaData { return $this->metaData; } diff --git a/src/Story/Helper/ComponentPreviewData.php b/src/Story/Helper/ComponentPreviewData.php index 4dcf1bb..63ac661 100644 --- a/src/Story/Helper/ComponentPreviewData.php +++ b/src/Story/Helper/ComponentPreviewData.php @@ -4,14 +4,14 @@ use Torr\Storyblok\Component\Data\ComponentData; use Torr\Storyblok\Exception\Story\InvalidDataException; -use Torr\Storyblok\Story\StoryMetaData; +use Torr\Storyblok\Story\MetaData\DocumentMetaData; final class ComponentPreviewData { /** * Normalizes the preview-related data to return it directly */ - public static function normalizeData (StoryMetaData|ComponentData $metaData) : array + public static function normalizeData (DocumentMetaData|ComponentData $metaData) : array { if ($metaData instanceof ComponentData) { diff --git a/src/Story/MetaData/BlokMetaData.php b/src/Story/MetaData/BlokMetaData.php new file mode 100644 index 0000000..9cf3140 --- /dev/null +++ b/src/Story/MetaData/BlokMetaData.php @@ -0,0 +1,46 @@ +uid = $data["_uid"]; + $this->type = $data["component"]; + $this->previewData = $data["_editable"]; + } + + /** + * + */ + public function getUid () : string + { + return $this->uid; + } + + /** + * + */ + public function getPreviewData () : ?string + { + return $this->previewData; + } + + /** + * + */ + public function getType () : string + { + return $this->type; + } +} diff --git a/src/Story/StoryMetaData.php b/src/Story/MetaData/DocumentMetaData.php similarity index 98% rename from src/Story/StoryMetaData.php rename to src/Story/MetaData/DocumentMetaData.php index 9452cf5..7789d71 100644 --- a/src/Story/StoryMetaData.php +++ b/src/Story/MetaData/DocumentMetaData.php @@ -1,11 +1,11 @@ Date: Mon, 6 Nov 2023 15:43:20 +0100 Subject: [PATCH 16/30] Remove obsolete classes --- src/Visitor/ComponentDataVisitorInterface.php | 17 ----------------- src/Visitor/DataVisitorInterface.php | 16 ---------------- 2 files changed, 33 deletions(-) delete mode 100644 src/Visitor/ComponentDataVisitorInterface.php delete mode 100644 src/Visitor/DataVisitorInterface.php diff --git a/src/Visitor/ComponentDataVisitorInterface.php b/src/Visitor/ComponentDataVisitorInterface.php deleted file mode 100644 index 2c8f272..0000000 --- a/src/Visitor/ComponentDataVisitorInterface.php +++ /dev/null @@ -1,17 +0,0 @@ - Date: Mon, 6 Nov 2023 15:44:09 +0100 Subject: [PATCH 17/30] Move data validator --- src/{ => Data}/Validator/DataValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ => Data}/Validator/DataValidator.php (97%) diff --git a/src/Validator/DataValidator.php b/src/Data/Validator/DataValidator.php similarity index 97% rename from src/Validator/DataValidator.php rename to src/Data/Validator/DataValidator.php index ea4121c..92d3256 100644 --- a/src/Validator/DataValidator.php +++ b/src/Data/Validator/DataValidator.php @@ -1,6 +1,6 @@ Date: Mon, 6 Nov 2023 15:44:22 +0100 Subject: [PATCH 18/30] Remove obsolete class --- src/Context/ComponentContext.php | 93 -------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 src/Context/ComponentContext.php diff --git a/src/Context/ComponentContext.php b/src/Context/ComponentContext.php deleted file mode 100644 index 89592df..0000000 --- a/src/Context/ComponentContext.php +++ /dev/null @@ -1,93 +0,0 @@ -storyblokIdSlugMapper = $storyblokIdSlugMapper; - } - - /** - * @see DataValidator::ensureDataIsValid() - * - * @param string[] $contentPath - * @param array $constraints - */ - public function ensureDataIsValid ( - array $contentPath, - FieldDefinition $field, - mixed $data, - array $constraints, - ) : void - { - $this->validator->ensureDataIsValid($contentPath, $field, $data, $constraints); - } - - /** - * @see ComponentManager::getComponent() - */ - public function getComponentByKey (string $key) : AbstractComponent - { - return $this->componentManager->getComponent($key); - } - - /** - * @see DataTransformer::normalizeOptionalString() - */ - public function normalizeOptionalString (?string $value) : ?string - { - return $this->dataTransformer->normalizeOptionalString($value); - } - - /** - * @return array{int|null, int|null} - */ - public function extractImageDimensions (string $imageUrl) : array - { - return $this->imageDimensionsExtractor->extractImageDimensions($imageUrl); - } - - /** - * @see StoryblokIdSlugMapper::getFullSlugById() - */ - public function fetchFullSlugByUuid (string|int $identifier) : string|null - { - // the content api is already set by the DI container - \assert(null !== $this->storyblokIdSlugMapper); - return $this->storyblokIdSlugMapper->getFullSlugById($identifier); - } -} From a9396eccaf7c79161c794fd00e6c06e521a771d9 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:44:26 +0100 Subject: [PATCH 19/30] CS --- src/Cache/DebugCache.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Cache/DebugCache.php b/src/Cache/DebugCache.php index 1bf054d..4241fdd 100644 --- a/src/Cache/DebugCache.php +++ b/src/Cache/DebugCache.php @@ -30,7 +30,6 @@ public function __construct ( */ public function get () : mixed { - if ($this->isDebug) { if ($this->hasLocaleCache) From 620a6dd7057f9d556bc6567d5a60b2cbdae1316d Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:44:37 +0100 Subject: [PATCH 20/30] Move folder data class --- src/{ => Data}/Folder/FolderData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ => Data}/Folder/FolderData.php (93%) diff --git a/src/Folder/FolderData.php b/src/Data/Folder/FolderData.php similarity index 93% rename from src/Folder/FolderData.php rename to src/Data/Folder/FolderData.php index bdbbe4a..0e013ec 100644 --- a/src/Folder/FolderData.php +++ b/src/Data/Folder/FolderData.php @@ -1,6 +1,6 @@ Date: Mon, 6 Nov 2023 15:44:57 +0100 Subject: [PATCH 21/30] Update namespaces after moving --- src/Api/ManagementApi.php | 2 +- src/Definition/Field/FieldDefinition.php | 2 +- src/Hydrator/StoryHydrator.php | 2 +- src/Mapping/Field/AbstractField.php | 2 +- src/Mapping/Field/BloksField.php | 2 +- src/Mapping/Field/NumberField.php | 2 +- src/Mapping/Field/TextField.php | 2 +- tests/Context/ComponentContextTestHelperTrait.php | 2 +- tests/Field/Definition/ChoiceFieldTest.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Api/ManagementApi.php b/src/Api/ManagementApi.php index 84b0d1f..f8ccfe6 100644 --- a/src/Api/ManagementApi.php +++ b/src/Api/ManagementApi.php @@ -12,7 +12,7 @@ use Torr\Storyblok\Api\Data\ComponentIdMap; use Torr\Storyblok\Config\StoryblokConfig; use Torr\Storyblok\Exception\Api\ApiRequestFailedException; -use Torr\Storyblok\Folder\FolderData; +use Torr\Storyblok\Data\Folder\FolderData; final class ManagementApi { diff --git a/src/Definition/Field/FieldDefinition.php b/src/Definition/Field/FieldDefinition.php index ee73fd7..772feb3 100644 --- a/src/Definition/Field/FieldDefinition.php +++ b/src/Definition/Field/FieldDefinition.php @@ -7,7 +7,7 @@ use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Mapping\Field\AbstractField; use Torr\Storyblok\Mapping\FieldAttribute\FieldAttributeInterface; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; final readonly class FieldDefinition { diff --git a/src/Hydrator/StoryHydrator.php b/src/Hydrator/StoryHydrator.php index 0752e1a..de54f4a 100644 --- a/src/Hydrator/StoryHydrator.php +++ b/src/Hydrator/StoryHydrator.php @@ -16,7 +16,7 @@ use Torr\Storyblok\Story\Document; use Torr\Storyblok\Story\MetaData\BlokMetaData; use Torr\Storyblok\Story\MetaData\DocumentMetaData; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; final class StoryHydrator { diff --git a/src/Mapping/Field/AbstractField.php b/src/Mapping/Field/AbstractField.php index e3678f4..6209db8 100644 --- a/src/Mapping/Field/AbstractField.php +++ b/src/Mapping/Field/AbstractField.php @@ -4,7 +4,7 @@ use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Hydrator\StoryHydrator; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; abstract class AbstractField { diff --git a/src/Mapping/Field/BloksField.php b/src/Mapping/Field/BloksField.php index d7c02f5..eb6f5a6 100644 --- a/src/Mapping/Field/BloksField.php +++ b/src/Mapping/Field/BloksField.php @@ -11,7 +11,7 @@ use Torr\Storyblok\Exception\InvalidFieldConfigurationException; use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Hydrator\StoryHydrator; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; #[\Attribute(\Attribute::TARGET_PROPERTY)] final class BloksField extends AbstractField diff --git a/src/Mapping/Field/NumberField.php b/src/Mapping/Field/NumberField.php index 9608729..638d9b0 100644 --- a/src/Mapping/Field/NumberField.php +++ b/src/Mapping/Field/NumberField.php @@ -8,7 +8,7 @@ use Torr\Storyblok\Exception\InvalidFieldConfigurationException; use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Hydrator\StoryHydrator; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; #[\Attribute(\Attribute::TARGET_PROPERTY)] final class NumberField extends AbstractField diff --git a/src/Mapping/Field/TextField.php b/src/Mapping/Field/TextField.php index 1efe8f9..b5f7aef 100644 --- a/src/Mapping/Field/TextField.php +++ b/src/Mapping/Field/TextField.php @@ -6,7 +6,7 @@ use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Hydrator\StoryHydrator; use Torr\Storyblok\Transformer\DataTransformer; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; #[\Attribute(\Attribute::TARGET_PROPERTY)] final class TextField extends AbstractField diff --git a/tests/Context/ComponentContextTestHelperTrait.php b/tests/Context/ComponentContextTestHelperTrait.php index 7a497d9..0b14144 100644 --- a/tests/Context/ComponentContextTestHelperTrait.php +++ b/tests/Context/ComponentContextTestHelperTrait.php @@ -8,7 +8,7 @@ use Torr\Storyblok\Image\ImageDimensionsExtractor; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Transformer\DataTransformer; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; trait ComponentContextTestHelperTrait { diff --git a/tests/Field/Definition/ChoiceFieldTest.php b/tests/Field/Definition/ChoiceFieldTest.php index f121335..5eee9f3 100644 --- a/tests/Field/Definition/ChoiceFieldTest.php +++ b/tests/Field/Definition/ChoiceFieldTest.php @@ -14,7 +14,7 @@ use Torr\Storyblok\Image\ImageDimensionsExtractor; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Transformer\DataTransformer; -use Torr\Storyblok\Validator\DataValidator; +use Torr\Storyblok\Data\Validator\DataValidator; class ChoiceFieldTest extends TestCase { From 64caa0f40e9347f771f96987d8183611efe79427 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:45:03 +0100 Subject: [PATCH 22/30] Remove obsolete class --- src/Component/AbstractComponent.php | 291 ---------------------------- 1 file changed, 291 deletions(-) delete mode 100644 src/Component/AbstractComponent.php diff --git a/src/Component/AbstractComponent.php b/src/Component/AbstractComponent.php deleted file mode 100644 index 13e9370..0000000 --- a/src/Component/AbstractComponent.php +++ /dev/null @@ -1,291 +0,0 @@ - - */ - abstract protected function configureFields () : array; - - /** - * Returns the type of this component - */ - abstract protected function getComponentType () : ComponentType; - - /** - * Returns the human-readable name of this component - */ - abstract public function getDisplayName () : string; - - /** - * Returns the component group display name - */ - public function getComponentGroup () : string|\BackedEnum|null - { - return null; - } - - /** - * Returns the tags of this component - * - * @return array - */ - public function getTags () : array - { - return []; - } - - /** - * Returns the class of story to create for this component. - * If null is returned here, you can't create a story for that component - * - * @return class-string|null - */ - public function getStoryClass () : ?string - { - return null; - } - - /** - * Receives the Storyblok data for the given field and transforms it for better usage - */ - public function transformData ( - array $data, - ComponentContext $dataContext, - ?DataVisitorInterface $dataVisitor = null, - ) : ComponentData - { - $transformedData = []; - - foreach ($this->getFields()->getRootFields() as $fieldName => $field) - { - $transformedFieldData = $field->transformData( - $data[$fieldName] ?? null, - $dataContext, - $data, - $dataVisitor, - ); - - if ($transformedFieldData instanceof InlinedTransformedData) - { - $transformedData = [ - ...$transformedData, - ...$transformedFieldData->data, - ]; - } - else - { - $transformedData[$fieldName] = $transformedFieldData; - } - } - - $componentData = new ComponentData( - $data["_uid"], - static::getKey(), - $transformedData, - previewData: $data["_editable"] ?? null, - ); - - if ($dataVisitor instanceof ComponentDataVisitorInterface) - { - $dataVisitor->onDataVisit($this, $componentData); - } - - return $componentData; - } - - /** - * Transforms the data of a single field - */ - public function transformField ( - array $data, - string $fieldName, - ComponentContext $dataContext, - ?DataVisitorInterface $dataVisitor = null, - ) : mixed - { - $field = $this->getFields()->getField($fieldName); - - return $field->transformData( - $data[$fieldName] ?? null, - $dataContext, - $data, - $dataVisitor, - ); - } - - - /** - * @throws InvalidDataException - */ - public function validateData ( - ComponentContext $context, - mixed $data, - array $contentPath = [], - ?string $label = null, - ) : void - { - $contentPath = [ - ...$contentPath, - \sprintf("Component(%s, '%s')", static::getKey(), $label ?? "n/a"), - ]; - - // validate base data - $context->validator->ensureDataIsValid( - $contentPath, - $this, - $data, - [ - new NotNull(), - new Type("array"), - new Collection( - fields: [ - "_uid" => [ - new NotNull(), - new Type("string"), - ], - "component" => [ - new NotNull(), - new Type("string"), - ], - ], - allowExtraFields: true, - allowMissingFields: false, - ), - ], - ); - - \assert(\is_array($data)); - - foreach ($this->getFields()->getRootFields() as $name => $field) - { - $fieldData = $data[$name] ?? null; - - $field->validateData( - $context, - [ - ...$contentPath, - \sprintf("Field(%s)", $name), - ], - $fieldData, - $data, - ); - } - } - - final protected function getFields () : FieldCollection - { - return $this->fields ??= new FieldCollection($this->configureFields()); - } - - - /** - * Normalizes the fields for usage in the management API - * - * @param array $fields - */ - private function normalizeFields ( - array $fields, - ) : array - { - if (empty($fields)) - { - throw new InvalidComponentConfigurationException(\sprintf( - "Invalid component '%s': can't have a component without fields", - static::class, - )); - } - - $managementDataApi = new ManagementApiData(); - - foreach ($fields as $key => $field) - { - if (ComponentHelper::isReservedKey($key)) - { - throw new InvalidComponentConfigurationException(\sprintf( - "Invalid component configuration '%s': can't use '%s' as field key, as that is a reserved key.", - static::class, - $key, - )); - } - - $field->registerManagementApiData($key, $managementDataApi); - } - - return $managementDataApi->getFullConfig(); - } - - - /** - * Transforms the data for the component - * - * @internal - */ - final public function toManagementApiData () : array - { - if (ComponentHelper::isReservedKey(static::getKey())) - { - throw new InvalidComponentConfigurationException(\sprintf( - "Invalid component configuration '%s': can't use '%s' as component key, as that is a reserved key.", - static::class, - static::getKey(), - )); - } - - $definition = $this->configureComponent(); - - return [ - "name" => static::getKey(), - "display_name" => $this->getDisplayName(), - "schema" => $this->normalizeFields($this->getFields()->getRootFields()), - "image" => $definition->previewScreenshotUrl, - "preview" => $definition->previewFieldName, - "preview_tmpl" => $definition->previewTemplate, - "real_name" => static::getKey(), - "color" => $definition->iconBackgroundColor, - "icon" => $definition->icon?->value, - ...$this->getComponentType()->generateManagementApiData(), - ]; - } -} From 33abf070dd643d8543e365ef89f64a2e987f0457 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:45:16 +0100 Subject: [PATCH 23/30] Inline reserved field key --- src/Component/ComponentHelper.php | 14 -------------- .../Component/ComponentDefinitionFactory.php | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 14 deletions(-) delete mode 100644 src/Component/ComponentHelper.php diff --git a/src/Component/ComponentHelper.php b/src/Component/ComponentHelper.php deleted file mode 100644 index f8c1387..0000000 --- a/src/Component/ComponentHelper.php +++ /dev/null @@ -1,14 +0,0 @@ -getName(), fields: $this->createFieldDefinitions($embedClass, allowEmbeds: false), ); + continue; } @@ -127,6 +128,15 @@ private function createFieldDefinitions ( continue; } + if ($this->isReservedFieldName($fieldDefinition->key)) + { + throw new InvalidComponentDefinitionException(\sprintf( + "Can't use reserved field name '%s' at '%s'", + $fieldDefinition->key, + $this->formatPropertyName($reflectionProperty), + )); + } + $definitions[$fieldDefinition->key] = new FieldDefinition( $fieldDefinition, $reflectionProperty->getName(), @@ -171,6 +181,15 @@ private function getSingleClassType (\ReflectionProperty $property) : \Reflectio } + /** + * Returns whether the field uses a reserved field name + */ + private function isReservedFieldName (string $key) : bool + { + return "component" === $key; + } + + /** * */ From bc7d9082a168e202ed63fbcd7a2a13dbba1651cd Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:46:55 +0100 Subject: [PATCH 24/30] CS --- src/Story/MetaData/BlokMetaData.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Story/MetaData/BlokMetaData.php b/src/Story/MetaData/BlokMetaData.php index 9cf3140..a61065e 100644 --- a/src/Story/MetaData/BlokMetaData.php +++ b/src/Story/MetaData/BlokMetaData.php @@ -9,15 +9,13 @@ final class BlokMetaData private readonly string $type; /** + * @param array{"_uid": string, "component": string, "_editable": string|null} $data */ - public function __construct ( - array $data - - ) + public function __construct (array $data) { $this->uid = $data["_uid"]; $this->type = $data["component"]; - $this->previewData = $data["_editable"]; + $this->previewData = $data["_editable"] ?? null; } /** From 8a5517fd0d4f56680865991a2d6a6fdc4ee2c03d Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:58:51 +0100 Subject: [PATCH 25/30] Remove component validator We now automatically validate when generating, so no need for a separate command --- src/Command/ValidateDefinitionsCommand.php | 62 -------------------- src/Manager/Validator/ComponentValidator.php | 31 ---------- 2 files changed, 93 deletions(-) delete mode 100644 src/Command/ValidateDefinitionsCommand.php delete mode 100644 src/Manager/Validator/ComponentValidator.php diff --git a/src/Command/ValidateDefinitionsCommand.php b/src/Command/ValidateDefinitionsCommand.php deleted file mode 100644 index e903451..0000000 --- a/src/Command/ValidateDefinitionsCommand.php +++ /dev/null @@ -1,62 +0,0 @@ -title("Storyblok: Sync Definitions"); - - $spaceInfo = $this->contentApi->getSpaceInfo(); - - $io->comment(\sprintf( - "Validating components for space %s (%d)\n%s", - $spaceInfo->getName(), - $spaceInfo->getId(), - $spaceInfo->getBackendDashboardUrl(), - )); - - try - { - $this->componentValidator->validateDefinitions($io); - - $io->newLine(2); - $io->success("All definitions validated."); - - return self::SUCCESS; - } - catch (ValidationFailedException $exception) - { - $io->comment(\sprintf("ERROR\n%s", $exception->getMessage())); - $io->error("Definitions validation failed"); - - return self::FAILURE; - } - } -} diff --git a/src/Manager/Validator/ComponentValidator.php b/src/Manager/Validator/ComponentValidator.php deleted file mode 100644 index 8cf4163..0000000 --- a/src/Manager/Validator/ComponentValidator.php +++ /dev/null @@ -1,31 +0,0 @@ -componentNormalizer->normalize($io); - } - catch (InvalidComponentConfigurationException|ApiRequestException $exception) - { - throw new ValidationFailedException($exception->getMessage(), previous: $exception); - } - } -} From da6f7649dade8f99e1a64f7394334ea5dc147508 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:58:56 +0100 Subject: [PATCH 26/30] Remove obsolete class --- src/Management/ManagementApiData.php | 35 ---------------------------- 1 file changed, 35 deletions(-) delete mode 100644 src/Management/ManagementApiData.php diff --git a/src/Management/ManagementApiData.php b/src/Management/ManagementApiData.php deleted file mode 100644 index 144e7b3..0000000 --- a/src/Management/ManagementApiData.php +++ /dev/null @@ -1,35 +0,0 @@ -fields)) - { - throw new InvalidComponentConfigurationException(\sprintf( - "Invalid component configuration: field key '%s' used more than once", - $key, - )); - } - - $config["pos"] = \count($this->fields); - $this->fields[$key] = $config; - } - - /** - * Returns the full config - */ - public function getFullConfig () : array - { - return $this->fields; - } -} From 9578af6e02fcc412f58f20cbc5a8c69edcc07d70 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 15:59:15 +0100 Subject: [PATCH 27/30] Unify blok meta data --- src/Hydrator/StoryHydrator.php | 6 ++++- src/Story/Helper/ComponentPreviewData.php | 14 ++--------- src/Story/MetaData/BlokMetaData.php | 30 +++++++++-------------- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/Hydrator/StoryHydrator.php b/src/Hydrator/StoryHydrator.php index de54f4a..94a587f 100644 --- a/src/Hydrator/StoryHydrator.php +++ b/src/Hydrator/StoryHydrator.php @@ -123,7 +123,11 @@ public function hydrateBlok ( \assert(\is_a($blokClass, Blok::class, true)); - $metaData = new BlokMetaData($data); + $metaData = new BlokMetaData( + $data["_uid"], + $data["component"], + $data["_editable"], + ); $blok = new $blokClass($metaData); return $this->mapDataToFields( diff --git a/src/Story/Helper/ComponentPreviewData.php b/src/Story/Helper/ComponentPreviewData.php index 63ac661..30d6034 100644 --- a/src/Story/Helper/ComponentPreviewData.php +++ b/src/Story/Helper/ComponentPreviewData.php @@ -2,26 +2,16 @@ namespace Torr\Storyblok\Story\Helper; -use Torr\Storyblok\Component\Data\ComponentData; use Torr\Storyblok\Exception\Story\InvalidDataException; -use Torr\Storyblok\Story\MetaData\DocumentMetaData; +use Torr\Storyblok\Story\MetaData\BlokMetaData; final class ComponentPreviewData { /** * Normalizes the preview-related data to return it directly */ - public static function normalizeData (DocumentMetaData|ComponentData $metaData) : array + public static function normalizeData (BlokMetaData $metaData) : array { - if ($metaData instanceof ComponentData) - { - return [ - "_uid" => $metaData->uid, - "_type" => $metaData->type, - "_preview" => self::parsePreviewTag($metaData->previewData), - ]; - } - return [ "_uid" => $metaData->getUuid(), "_type" => $metaData->getType(), diff --git a/src/Story/MetaData/BlokMetaData.php b/src/Story/MetaData/BlokMetaData.php index a61065e..d49e2d2 100644 --- a/src/Story/MetaData/BlokMetaData.php +++ b/src/Story/MetaData/BlokMetaData.php @@ -2,43 +2,37 @@ namespace Torr\Storyblok\Story\MetaData; -final class BlokMetaData +class BlokMetaData { - private readonly string $uid; - private readonly ?string $previewData; - private readonly string $type; - /** - * @param array{"_uid": string, "component": string, "_editable": string|null} $data */ - public function __construct (array $data) - { - $this->uid = $data["_uid"]; - $this->type = $data["component"]; - $this->previewData = $data["_editable"] ?? null; - } + public function __construct ( + private readonly string $uuid, + private readonly string $type, + private readonly ?string $previewData = null, + ) {} /** * */ - public function getUid () : string + public function getUuid () : string { - return $this->uid; + return $this->uuid; } /** * */ - public function getPreviewData () : ?string + public function getType () : string { - return $this->previewData; + return $this->type; } /** * */ - public function getType () : string + public function getPreviewData () : ?string { - return $this->type; + return $this->previewData; } } From e09ab0825d9fd3637f0d8b06c04e80655215ee16 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 17:08:21 +0100 Subject: [PATCH 28/30] Unify attributes and depend and extended classes --- src/Api/ContentApi.php | 8 +-- src/Api/Data/PaginatedApiResult.php | 4 +- src/Component/Data/ComponentData.php | 18 ------- .../Component/ComponentDefinition.php | 21 +++++--- .../Component/ComponentDefinitionFactory.php | 20 +++----- ...ollectComponentDefinitionsCompilerPass.php | 21 ++++---- src/Hydrator/StoryHydrator.php | 16 +++--- src/Manager/ComponentManager.php | 6 +-- .../Normalizer/ComponentNormalizer.php | 49 ------------------- src/Manager/Sync/ComponentSync.php | 18 +++++-- src/Mapping/Field/AbstractField.php | 15 +++++- src/Mapping/Field/BloksField.php | 2 +- src/Mapping/Field/TextField.php | 2 +- src/Mapping/StoryDocument.php | 41 ---------------- src/Mapping/{StoryBlok.php => Storyblok.php} | 26 +--------- src/Story/Blok.php | 24 --------- src/Story/Document.php | 36 -------------- src/Story/Helper/ComponentPreviewData.php | 4 +- .../{BlokMetaData.php => ContentMetaData.php} | 2 +- src/Story/MetaData/DocumentMetaData.php | 33 +++---------- src/Story/StoryContent.php | 35 +++++++++++++ src/Story/StoryDocument.php | 36 ++++++++++++++ src/TorrStoryblokBundle.php | 2 +- 23 files changed, 162 insertions(+), 277 deletions(-) delete mode 100644 src/Component/Data/ComponentData.php delete mode 100644 src/Manager/Normalizer/ComponentNormalizer.php delete mode 100644 src/Mapping/StoryDocument.php rename src/Mapping/{StoryBlok.php => Storyblok.php} (50%) delete mode 100644 src/Story/Blok.php delete mode 100644 src/Story/Document.php rename src/Story/MetaData/{BlokMetaData.php => ContentMetaData.php} (95%) create mode 100644 src/Story/StoryContent.php create mode 100644 src/Story/StoryDocument.php diff --git a/src/Api/ContentApi.php b/src/Api/ContentApi.php index 57fe2be..fac6515 100644 --- a/src/Api/ContentApi.php +++ b/src/Api/ContentApi.php @@ -16,7 +16,7 @@ use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Release\ReleaseVersion; -use Torr\Storyblok\Story\Document; +use Torr\Storyblok\Story\StoryDocument; use Torr\Storyblok\Hydrator\StoryHydrator; final class ContentApi implements ResetInterface @@ -54,7 +54,7 @@ public function __construct ( public function fetchSingleStory ( string|int $identifier, ReleaseVersion $version = ReleaseVersion::PUBLISHED, - ) : ?Document + ) : ?StoryDocument { try { @@ -102,7 +102,7 @@ public function fetchSingleStory ( * This method provides certain commonly used named parameters, but also supports passing arbitrary parameters * in the parameter. Passing named parameters will always overwrite parameters in $query. * - * @template TStory of Document + * @template TStory of StoryDocument * * @param class-string $storyType * @@ -154,7 +154,7 @@ public function fetchStories ( * * @param string|string[]|null $slug * - * @return array + * @return array *@throws ContentRequestFailedException * */ diff --git a/src/Api/Data/PaginatedApiResult.php b/src/Api/Data/PaginatedApiResult.php index 0884b25..242e323 100644 --- a/src/Api/Data/PaginatedApiResult.php +++ b/src/Api/Data/PaginatedApiResult.php @@ -2,12 +2,12 @@ namespace Torr\Storyblok\Api\Data; -use Torr\Storyblok\Story\Document; +use Torr\Storyblok\Story\StoryDocument; final class PaginatedApiResult { /** - * @param array $stories + * @param array $stories */ public function __construct ( public readonly int $perPage, diff --git a/src/Component/Data/ComponentData.php b/src/Component/Data/ComponentData.php deleted file mode 100644 index cfb83d5..0000000 --- a/src/Component/Data/ComponentData.php +++ /dev/null @@ -1,18 +0,0 @@ - */ + public Storyblok $definition, + /** @type class-string */ public string $storyClass, + public bool $isDocument, /** @type array */ public array $fields, ) @@ -42,6 +43,13 @@ public function getName () : string ->toString(); } + /** + */ + public function getKey () : string + { + return $this->definition->key; + } + public function generateManagementApiData () : array { @@ -79,7 +87,8 @@ public function generateManagementApiData () : array "display_name" => $this->getName(), "schema" => $fieldSchemas, "preview_field" => $this->definition->previewField, - ...$this->definition->getComponentTypeApiData(), + "is_root" => $this->isDocument, + "is_nestable" => !$this->isDocument, ]; } } diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php index e06ba08..97ac208 100644 --- a/src/Definition/Component/ComponentDefinitionFactory.php +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -9,8 +9,9 @@ use Torr\Storyblok\Mapping\Embed\EmbeddedStory; use Torr\Storyblok\Mapping\Field\AbstractField; use Torr\Storyblok\Mapping\FieldAttribute\FieldAttributeInterface; -use Torr\Storyblok\Mapping\StoryBlok; -use Torr\Storyblok\Mapping\StoryDocument; +use Torr\Storyblok\Mapping\Storyblok; +use Torr\Storyblok\Story\StoryContent; +use Torr\Storyblok\Story\StoryDocument; final readonly class ComponentDefinitionFactory { @@ -49,22 +50,15 @@ private function createDefinition ( try { $reflectionClass = new \ReflectionClass($storyblokClass); - $definition = $this->helper->getOptionalSingleAttribute($reflectionClass, StoryDocument::class) - ?? $this->helper->getOptionalSingleAttribute($reflectionClass, StoryBlok::class); + $definition = $this->helper->getRequiredSingleAttribute($reflectionClass, Storyblok::class); - if (null === $definition) - { - throw new InvalidComponentDefinitionException(\sprintf( - "Could not find required attribute of type '%s' or '%s' on class '%s'.", - StoryDocument::class, - StoryBlok::class, - $reflectionClass->getName(), - )); - } + $isDocument = \is_a($reflectionClass->getName(), StoryDocument::class, true); + \assert($isDocument || \is_a($reflectionClass->getName(), StoryContent::class, true), "must either be a document or a blok"); return new ComponentDefinition( $definition, $storyblokClass, + $isDocument, $this->createFieldDefinitions($reflectionClass), ); } diff --git a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php index e0a5171..ed13f15 100644 --- a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php +++ b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php @@ -7,9 +7,9 @@ use Torr\Storyblok\Exception\Component\DuplicateComponentKeyException; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Manager\ComponentManager; -use Torr\Storyblok\Mapping\StoryBlok; -use Torr\Storyblok\Mapping\StoryDocument; -use Torr\Storyblok\Story\Document; +use Torr\Storyblok\Mapping\Storyblok; +use Torr\Storyblok\Story\StoryContent; +use Torr\Storyblok\Story\StoryDocument; final class CollectComponentDefinitionsCompilerPass implements CompilerPassInterface { @@ -37,12 +37,13 @@ public function process (ContainerBuilder $container) : void continue; } - if (!\is_a($class, $attribute->getRequiredExtendedClass(), true)) + if (!\is_a($class, StoryDocument::class, true) && !\is_a($class, StoryContent::class, true)) { throw new InvalidComponentDefinitionException(\sprintf( - "Storyblok element '%s' must extend %s.", + "Storyblok element '%s' must extend either '%s' or '%s'.", $class, - $attribute->getRequiredExtendedClass(), + StoryDocument::class, + StoryContent::class, )); } @@ -70,14 +71,12 @@ public function process (ContainerBuilder $container) : void * * @param class-string $class */ - private function extractKey (string $class) : StoryDocument|StoryBlok|null + private function extractKey (string $class) : ?Storyblok { try { $reflectionClass = new \ReflectionClass($class); - $reflectionAttribute = $reflectionClass->getAttributes(StoryDocument::class)[0] - ?? $reflectionClass->getAttributes(StoryBlok::class)[0] - ?? null; + $reflectionAttribute = $reflectionClass->getAttributes(Storyblok::class)[0] ?? null; if (null === $reflectionAttribute) { @@ -85,7 +84,7 @@ private function extractKey (string $class) : StoryDocument|StoryBlok|null } $attribute = $reflectionAttribute->newInstance(); - \assert($attribute instanceof StoryDocument || $attribute instanceof StoryBlok); + \assert($attribute instanceof Storyblok); return $attribute; } diff --git a/src/Hydrator/StoryHydrator.php b/src/Hydrator/StoryHydrator.php index 94a587f..40fa0a1 100644 --- a/src/Hydrator/StoryHydrator.php +++ b/src/Hydrator/StoryHydrator.php @@ -12,9 +12,9 @@ use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Mapping\Embed\EmbeddedStory; use Torr\Storyblok\Mapping\Field\BloksField; -use Torr\Storyblok\Story\Blok; -use Torr\Storyblok\Story\Document; -use Torr\Storyblok\Story\MetaData\BlokMetaData; +use Torr\Storyblok\Story\StoryContent; +use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\MetaData\ContentMetaData; use Torr\Storyblok\Story\MetaData\DocumentMetaData; use Torr\Storyblok\Data\Validator\DataValidator; @@ -91,12 +91,12 @@ public function createFromApiData (array $data) : ?object public function hydrateDocument ( string $type, array $data, - ) : Document + ) : StoryDocument { $definition = $this->componentManager->getDefinition($type); $storyClass = $definition->storyClass; - \assert(\is_a($storyClass, Document::class, true)); + \assert(\is_a($storyClass, StoryDocument::class, true)); $metaData = new DocumentMetaData($data, $type); $document = new $storyClass($metaData); @@ -116,14 +116,14 @@ public function hydrateBlok ( array $contentPath, string $type, array $data, - ) : Blok + ) : StoryContent { $definition = $this->componentManager->getDefinition($type); $blokClass = $definition->storyClass; - \assert(\is_a($blokClass, Blok::class, true)); + \assert(\is_a($blokClass, StoryContent::class, true)); - $metaData = new BlokMetaData( + $metaData = new ContentMetaData( $data["_uid"], $data["component"], $data["_editable"], diff --git a/src/Manager/ComponentManager.php b/src/Manager/ComponentManager.php index ad2c740..109c866 100644 --- a/src/Manager/ComponentManager.php +++ b/src/Manager/ComponentManager.php @@ -13,8 +13,8 @@ use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; use Torr\Storyblok\Exception\Component\UnknownStoryTypeException; -use Torr\Storyblok\Mapping\StoryDocument; -use Torr\Storyblok\Story\Document; +use Torr\Storyblok\Mapping\Storyblok; +use Torr\Storyblok\Story\StoryDocument; /** * @final @@ -65,7 +65,7 @@ public function getAllComponents () : array /** * Returns the first component that creates a story of the given type * - * @template TStory of Document + * @template TStory of StoryDocument * * @param class-string $storyType * diff --git a/src/Manager/Normalizer/ComponentNormalizer.php b/src/Manager/Normalizer/ComponentNormalizer.php deleted file mode 100644 index 7312c0c..0000000 --- a/src/Manager/Normalizer/ComponentNormalizer.php +++ /dev/null @@ -1,49 +0,0 @@ -componentManager->getDefinitions(); - - // normalize everything to check if normalization fails - foreach ($definitions->getComponents() as $component) - { - $definition = $component->definition; - - $key = \sprintf( - "%s (%s) ... ", - $definition->name, - $definition->key, - ); - - $io->write("Normalizing {$key} "); - - $normalized[$key] = new ComponentImport( - $component->generateManagementApiData(), - $definition->group, - ); - - $io->writeln("done ✓"); - } - - return $normalized; - } -} diff --git a/src/Manager/Sync/ComponentSync.php b/src/Manager/Sync/ComponentSync.php index d31486a..7e6276c 100644 --- a/src/Manager/Sync/ComponentSync.php +++ b/src/Manager/Sync/ComponentSync.php @@ -5,11 +5,11 @@ use Torr\Cli\Console\Style\TorrStyle; use Torr\Storyblok\Api\Data\ComponentImport; use Torr\Storyblok\Api\ManagementApi; +use Torr\Storyblok\Definition\Component\ComponentDefinition; use Torr\Storyblok\Exception\Api\ApiRequestException; use Torr\Storyblok\Exception\InvalidComponentConfigurationException; use Torr\Storyblok\Exception\Sync\SyncFailedException; use Torr\Storyblok\Manager\ComponentManager; -use Torr\Storyblok\Manager\Normalizer\ComponentNormalizer; final class ComponentSync { @@ -17,7 +17,7 @@ final class ComponentSync */ public function __construct ( private readonly ManagementApi $managementApi, - private readonly ComponentNormalizer $componentNormalizer, + private readonly ComponentManager $componentManager, ) {} /** @@ -32,7 +32,19 @@ public function syncDefinitions ( { try { - $normalized = $this->componentNormalizer->normalize($io); + $normalized = []; + $registry = $this->componentManager->getDefinitions(); + + $io->listing( + \array_map( + static fn (ComponentDefinition $definition) => \sprintf( + "%s (%s) ... ", + $definition->getName(), + $definition->getKey(), + ), + $registry->getComponents(), + ), + ); if (!$sync) { diff --git a/src/Mapping/Field/AbstractField.php b/src/Mapping/Field/AbstractField.php index 6209db8..59804f0 100644 --- a/src/Mapping/Field/AbstractField.php +++ b/src/Mapping/Field/AbstractField.php @@ -5,13 +5,14 @@ use Torr\Storyblok\Field\FieldType; use Torr\Storyblok\Hydrator\StoryHydrator; use Torr\Storyblok\Data\Validator\DataValidator; +use function Symfony\Component\String\u; abstract class AbstractField { public function __construct ( public readonly FieldType $internalStoryblokType, public readonly string $key, - public readonly string $label, + public readonly ?string $label = null, public readonly mixed $defaultValue = null, ) {} @@ -23,11 +24,21 @@ public function generateManagementApiData () : array { return [ "type" => $this->internalStoryblokType->value, - "display_name" => $this->label, + "display_name" => $this->getLabel(), "default_value" => $this->defaultValue, ]; } + /** + * + */ + public function getLabel () : string + { + return $this->label ?? u($this->key) + ->title() + ->toString(); + } + /** * Transforms the raw data from Storyblok to a sanitized format. */ diff --git a/src/Mapping/Field/BloksField.php b/src/Mapping/Field/BloksField.php index eb6f5a6..485cc84 100644 --- a/src/Mapping/Field/BloksField.php +++ b/src/Mapping/Field/BloksField.php @@ -18,7 +18,7 @@ final class BloksField extends AbstractField { public function __construct ( string $key, - string $label, + ?string $label = null, private readonly ?int $minimumNumberOfBloks = null, private readonly ?int $maximumNumberOfBloks = null, private readonly ComponentFilter $allowedComponents = new ComponentFilter(), diff --git a/src/Mapping/Field/TextField.php b/src/Mapping/Field/TextField.php index b5f7aef..f620cbf 100644 --- a/src/Mapping/Field/TextField.php +++ b/src/Mapping/Field/TextField.php @@ -13,7 +13,7 @@ final class TextField extends AbstractField { public function __construct ( string $key, - string $label, + ?string $label = null, ?string $defaultValue = null, private readonly bool $multiline = false, private readonly ?int $maxLength = null, diff --git a/src/Mapping/StoryDocument.php b/src/Mapping/StoryDocument.php deleted file mode 100644 index f9be57c..0000000 --- a/src/Mapping/StoryDocument.php +++ /dev/null @@ -1,41 +0,0 @@ - */ - public array $tags = [], - public ?string $previewField = null, - public string|\BackedEnum|null $group = null, - ) {} - - /** - * - */ - public function getComponentTypeApiData () : array - { - return [ - "is_root" => true, - "is_nestable" => false, - ]; - } - - - /** - * @return class-string - */ - public function getRequiredExtendedClass () : string - { - return Document::class; - } -} diff --git a/src/Mapping/StoryBlok.php b/src/Mapping/Storyblok.php similarity index 50% rename from src/Mapping/StoryBlok.php rename to src/Mapping/Storyblok.php index 0a51d08..78e7f37 100644 --- a/src/Mapping/StoryBlok.php +++ b/src/Mapping/Storyblok.php @@ -2,13 +2,11 @@ namespace Torr\Storyblok\Mapping; -use Torr\Storyblok\Story\Blok; - /** - * A nestable storyblok content element + * A storyblok content element */ #[\Attribute(\Attribute::TARGET_CLASS)] -final class StoryBlok +final readonly class Storyblok { public function __construct ( public string $key, @@ -18,24 +16,4 @@ public function __construct ( public ?string $previewField = null, public string|\BackedEnum|null $group = null, ) {} - - /** - * - */ - public function getComponentTypeApiData () : array - { - return [ - "is_root" => false, - "is_nestable" => true, - ]; - } - - - /** - * @return class-string - */ - public function getRequiredExtendedClass () : string - { - return Blok::class; - } } diff --git a/src/Story/Blok.php b/src/Story/Blok.php deleted file mode 100644 index aff17b2..0000000 --- a/src/Story/Blok.php +++ /dev/null @@ -1,24 +0,0 @@ -metaData; - } -} - diff --git a/src/Story/Document.php b/src/Story/Document.php deleted file mode 100644 index 1792fe0..0000000 --- a/src/Story/Document.php +++ /dev/null @@ -1,36 +0,0 @@ -metaData->getUuid(); - } - - /** - */ - final public function getMetaData () : DocumentMetaData - { - return $this->metaData; - } - - /** - */ - final public function getFullSlug () : string - { - return $this->metaData->getFullSlug(); - } -} diff --git a/src/Story/Helper/ComponentPreviewData.php b/src/Story/Helper/ComponentPreviewData.php index 30d6034..06dedaa 100644 --- a/src/Story/Helper/ComponentPreviewData.php +++ b/src/Story/Helper/ComponentPreviewData.php @@ -3,14 +3,14 @@ namespace Torr\Storyblok\Story\Helper; use Torr\Storyblok\Exception\Story\InvalidDataException; -use Torr\Storyblok\Story\MetaData\BlokMetaData; +use Torr\Storyblok\Story\MetaData\ContentMetaData; final class ComponentPreviewData { /** * Normalizes the preview-related data to return it directly */ - public static function normalizeData (BlokMetaData $metaData) : array + public static function normalizeData (ContentMetaData $metaData) : array { return [ "_uid" => $metaData->getUuid(), diff --git a/src/Story/MetaData/BlokMetaData.php b/src/Story/MetaData/ContentMetaData.php similarity index 95% rename from src/Story/MetaData/BlokMetaData.php rename to src/Story/MetaData/ContentMetaData.php index d49e2d2..7763f38 100644 --- a/src/Story/MetaData/BlokMetaData.php +++ b/src/Story/MetaData/ContentMetaData.php @@ -2,7 +2,7 @@ namespace Torr\Storyblok\Story\MetaData; -class BlokMetaData +class ContentMetaData { /** */ diff --git a/src/Story/MetaData/DocumentMetaData.php b/src/Story/MetaData/DocumentMetaData.php index 7789d71..5008570 100644 --- a/src/Story/MetaData/DocumentMetaData.php +++ b/src/Story/MetaData/DocumentMetaData.php @@ -5,23 +5,22 @@ use Torr\Storyblok\Exception\Story\StoryHydrationFailed; use Torr\Storyblok\Translation\LocaleHelper; -final class DocumentMetaData +final class DocumentMetaData extends ContentMetaData { private readonly array $data; private readonly array $slugSegments; - private readonly ?string $previewData; /** */ public function __construct ( array $data, - /** - * The component type of the story's component - */ - private readonly string $type, ) { - $this->previewData = $data["content"]["_editable"] ?? null; + parent::__construct( + $data["uuid"], + $data["content"]["component"], + $data["content"]["_editable"] ?? null, + ); unset($data["content"]); $this->data = $data; $this->slugSegments = \explode("/", \rtrim($data["full_slug"], "/")); @@ -61,14 +60,6 @@ public function getId () : int return $this->data["id"]; } - /** - * - */ - public function getUuid () : string - { - return $this->data["uuid"]; - } - /** * */ @@ -101,13 +92,6 @@ public function getLocale () : string return $this->data["lang"]; } - /** - */ - public function getType () : string - { - return $this->type; - } - /** * Returns the slug of the parent * (without trailing slash). @@ -209,9 +193,4 @@ public function getTranslatedDocumentsMapping () : array return $mapping; } - - public function getPreviewData () : ?string - { - return $this->previewData; - } } diff --git a/src/Story/StoryContent.php b/src/Story/StoryContent.php new file mode 100644 index 0000000..a7fd5a2 --- /dev/null +++ b/src/Story/StoryContent.php @@ -0,0 +1,35 @@ +metaData->getUuid(); + } + + + /** + */ + public function getMetaData () : ContentMetaData + { + return $this->metaData; + } +} + diff --git a/src/Story/StoryDocument.php b/src/Story/StoryDocument.php new file mode 100644 index 0000000..12cddd5 --- /dev/null +++ b/src/Story/StoryDocument.php @@ -0,0 +1,36 @@ +metaData instanceof DocumentMetaData); + return $this->metaData; + } + + /** + */ + final public function getFullSlug () : string + { + return $this->getMetaData()->getFullSlug(); + } +} diff --git a/src/TorrStoryblokBundle.php b/src/TorrStoryblokBundle.php index 880aa99..7fe3271 100644 --- a/src/TorrStoryblokBundle.php +++ b/src/TorrStoryblokBundle.php @@ -10,7 +10,7 @@ use Torr\Storyblok\DependencyInjection\CollectComponentDefinitionsCompilerPass; use Torr\Storyblok\DependencyInjection\StoryblokBundleConfiguration; use Torr\Storyblok\DependencyInjection\StoryblokBundleExtension; -use Torr\Storyblok\Story\Document; +use Torr\Storyblok\Story\StoryDocument; final class TorrStoryblokBundle extends Bundle { From eab6ec78ae0726cadfaa622616b68625e1f819e4 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 6 Nov 2023 17:27:02 +0100 Subject: [PATCH 29/30] Move helper functions --- src/Definition/Component/ComponentDefinition.php | 6 +----- .../Component/ComponentDefinitionFactory.php | 2 +- .../Component/ComponentDefinitionRegistry.php | 2 +- .../{Component => }/Reflection/ReflectionHelper.php | 2 +- src/Mapping/Storyblok.php | 13 +++++++++++++ 5 files changed, 17 insertions(+), 8 deletions(-) rename src/Definition/{Component => }/Reflection/ReflectionHelper.php (97%) diff --git a/src/Definition/Component/ComponentDefinition.php b/src/Definition/Component/ComponentDefinition.php index b2e19b1..3e34cca 100644 --- a/src/Definition/Component/ComponentDefinition.php +++ b/src/Definition/Component/ComponentDefinition.php @@ -8,7 +8,6 @@ use Torr\Storyblok\Mapping\Storyblok; use Torr\Storyblok\Story\StoryContent; use Torr\Storyblok\Story\StoryDocument; -use function Symfony\Component\String\u; final readonly class ComponentDefinition { @@ -37,10 +36,7 @@ public function __construct ( */ public function getName () : string { - return $this->definition->name - ?? u($this->definition->key) - ->title() - ->toString(); + return $this->definition->getName(); } /** diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php index 97ac208..a3cf8f6 100644 --- a/src/Definition/Component/ComponentDefinitionFactory.php +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -2,7 +2,7 @@ namespace Torr\Storyblok\Definition\Component; -use Torr\Storyblok\Definition\Component\Reflection\ReflectionHelper; +use Torr\Storyblok\Definition\Reflection\ReflectionHelper; use Torr\Storyblok\Definition\Field\EmbeddedFieldDefinition; use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; diff --git a/src/Definition/Component/ComponentDefinitionRegistry.php b/src/Definition/Component/ComponentDefinitionRegistry.php index c5071ff..5c7d937 100644 --- a/src/Definition/Component/ComponentDefinitionRegistry.php +++ b/src/Definition/Component/ComponentDefinitionRegistry.php @@ -24,7 +24,7 @@ public function __construct (array $definitions) // sort components by name \uasort( $definitions, - static fn (ComponentDefinition $left, ComponentDefinition $right) => \strnatcasecmp($left->definition->name, $right->definition->name), + static fn (ComponentDefinition $left, ComponentDefinition $right) => \strnatcasecmp($left->getName(), $right->getName()), ); $this->definitions = $definitions; diff --git a/src/Definition/Component/Reflection/ReflectionHelper.php b/src/Definition/Reflection/ReflectionHelper.php similarity index 97% rename from src/Definition/Component/Reflection/ReflectionHelper.php rename to src/Definition/Reflection/ReflectionHelper.php index aef028f..1e25bce 100644 --- a/src/Definition/Component/Reflection/ReflectionHelper.php +++ b/src/Definition/Reflection/ReflectionHelper.php @@ -1,6 +1,6 @@ name ?? u($this->key) + ->title() + ->toString(); + } } From 912e5848445461498e01697e7f653f1b68947775 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 7 Nov 2023 11:04:05 +0100 Subject: [PATCH 30/30] Improve class names --- src/Api/ContentApi.php | 8 ++++---- src/Api/Data/PaginatedApiResult.php | 4 ++-- .../Component/ComponentDefinition.php | 6 +++--- .../Component/ComponentDefinitionFactory.php | 8 ++++---- ...ollectComponentDefinitionsCompilerPass.php | 10 +++++----- src/Hydrator/StoryHydrator.php | 20 +++++++++---------- src/Manager/ComponentManager.php | 4 ++-- src/Story/Helper/ComponentPreviewData.php | 4 ++-- ...ntMetaData.php => NestedStoryMetaData.php} | 2 +- ...taData.php => StandaloneStoryMetaData.php} | 2 +- .../{StoryContent.php => NestedStory.php} | 8 ++++---- ...Document.php => StandaloneNestedStory.php} | 10 +++++----- src/TorrStoryblokBundle.php | 2 +- 13 files changed, 44 insertions(+), 44 deletions(-) rename src/Story/MetaData/{ContentMetaData.php => NestedStoryMetaData.php} (95%) rename src/Story/MetaData/{DocumentMetaData.php => StandaloneStoryMetaData.php} (98%) rename src/Story/{StoryContent.php => NestedStory.php} (62%) rename src/Story/{StoryDocument.php => StandaloneNestedStory.php} (57%) diff --git a/src/Api/ContentApi.php b/src/Api/ContentApi.php index fac6515..5a43a61 100644 --- a/src/Api/ContentApi.php +++ b/src/Api/ContentApi.php @@ -16,7 +16,7 @@ use Torr\Storyblok\Exception\Story\InvalidDataException; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Release\ReleaseVersion; -use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\StandaloneNestedStory; use Torr\Storyblok\Hydrator\StoryHydrator; final class ContentApi implements ResetInterface @@ -54,7 +54,7 @@ public function __construct ( public function fetchSingleStory ( string|int $identifier, ReleaseVersion $version = ReleaseVersion::PUBLISHED, - ) : ?StoryDocument + ) : ?StandaloneNestedStory { try { @@ -102,7 +102,7 @@ public function fetchSingleStory ( * This method provides certain commonly used named parameters, but also supports passing arbitrary parameters * in the parameter. Passing named parameters will always overwrite parameters in $query. * - * @template TStory of StoryDocument + * @template TStory of StandaloneNestedStory * * @param class-string $storyType * @@ -154,7 +154,7 @@ public function fetchStories ( * * @param string|string[]|null $slug * - * @return array + * @return array *@throws ContentRequestFailedException * */ diff --git a/src/Api/Data/PaginatedApiResult.php b/src/Api/Data/PaginatedApiResult.php index 242e323..4f5aa86 100644 --- a/src/Api/Data/PaginatedApiResult.php +++ b/src/Api/Data/PaginatedApiResult.php @@ -2,12 +2,12 @@ namespace Torr\Storyblok\Api\Data; -use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\StandaloneNestedStory; final class PaginatedApiResult { /** - * @param array $stories + * @param array $stories */ public function __construct ( public readonly int $perPage, diff --git a/src/Definition/Component/ComponentDefinition.php b/src/Definition/Component/ComponentDefinition.php index 3e34cca..e079a03 100644 --- a/src/Definition/Component/ComponentDefinition.php +++ b/src/Definition/Component/ComponentDefinition.php @@ -6,8 +6,8 @@ use Torr\Storyblok\Definition\Field\FieldDefinition; use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Mapping\Storyblok; -use Torr\Storyblok\Story\StoryContent; -use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\NestedStory; +use Torr\Storyblok\Story\StandaloneNestedStory; final readonly class ComponentDefinition { @@ -15,7 +15,7 @@ */ public function __construct ( public Storyblok $definition, - /** @type class-string */ + /** @type class-string */ public string $storyClass, public bool $isDocument, /** @type array */ diff --git a/src/Definition/Component/ComponentDefinitionFactory.php b/src/Definition/Component/ComponentDefinitionFactory.php index a3cf8f6..1edf4e2 100644 --- a/src/Definition/Component/ComponentDefinitionFactory.php +++ b/src/Definition/Component/ComponentDefinitionFactory.php @@ -10,8 +10,8 @@ use Torr\Storyblok\Mapping\Field\AbstractField; use Torr\Storyblok\Mapping\FieldAttribute\FieldAttributeInterface; use Torr\Storyblok\Mapping\Storyblok; -use Torr\Storyblok\Story\StoryContent; -use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\NestedStory; +use Torr\Storyblok\Story\StandaloneNestedStory; final readonly class ComponentDefinitionFactory { @@ -52,8 +52,8 @@ private function createDefinition ( $reflectionClass = new \ReflectionClass($storyblokClass); $definition = $this->helper->getRequiredSingleAttribute($reflectionClass, Storyblok::class); - $isDocument = \is_a($reflectionClass->getName(), StoryDocument::class, true); - \assert($isDocument || \is_a($reflectionClass->getName(), StoryContent::class, true), "must either be a document or a blok"); + $isDocument = \is_a($reflectionClass->getName(), StandaloneNestedStory::class, true); + \assert($isDocument || \is_a($reflectionClass->getName(), NestedStory::class, true), "must either be a document or a blok"); return new ComponentDefinition( $definition, diff --git a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php index ed13f15..7ec61f6 100644 --- a/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php +++ b/src/DependencyInjection/CollectComponentDefinitionsCompilerPass.php @@ -8,8 +8,8 @@ use Torr\Storyblok\Exception\Component\InvalidComponentDefinitionException; use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Mapping\Storyblok; -use Torr\Storyblok\Story\StoryContent; -use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\NestedStory; +use Torr\Storyblok\Story\StandaloneNestedStory; final class CollectComponentDefinitionsCompilerPass implements CompilerPassInterface { @@ -37,13 +37,13 @@ public function process (ContainerBuilder $container) : void continue; } - if (!\is_a($class, StoryDocument::class, true) && !\is_a($class, StoryContent::class, true)) + if (!\is_a($class, StandaloneNestedStory::class, true) && !\is_a($class, NestedStory::class, true)) { throw new InvalidComponentDefinitionException(\sprintf( "Storyblok element '%s' must extend either '%s' or '%s'.", $class, - StoryDocument::class, - StoryContent::class, + StandaloneNestedStory::class, + NestedStory::class, )); } diff --git a/src/Hydrator/StoryHydrator.php b/src/Hydrator/StoryHydrator.php index 40fa0a1..33dcfa1 100644 --- a/src/Hydrator/StoryHydrator.php +++ b/src/Hydrator/StoryHydrator.php @@ -12,10 +12,10 @@ use Torr\Storyblok\Manager\ComponentManager; use Torr\Storyblok\Mapping\Embed\EmbeddedStory; use Torr\Storyblok\Mapping\Field\BloksField; -use Torr\Storyblok\Story\StoryContent; -use Torr\Storyblok\Story\StoryDocument; -use Torr\Storyblok\Story\MetaData\ContentMetaData; -use Torr\Storyblok\Story\MetaData\DocumentMetaData; +use Torr\Storyblok\Story\NestedStory; +use Torr\Storyblok\Story\StandaloneNestedStory; +use Torr\Storyblok\Story\MetaData\NestedStoryMetaData; +use Torr\Storyblok\Story\MetaData\StandaloneStoryMetaData; use Torr\Storyblok\Data\Validator\DataValidator; final class StoryHydrator @@ -91,14 +91,14 @@ public function createFromApiData (array $data) : ?object public function hydrateDocument ( string $type, array $data, - ) : StoryDocument + ) : StandaloneNestedStory { $definition = $this->componentManager->getDefinition($type); $storyClass = $definition->storyClass; - \assert(\is_a($storyClass, StoryDocument::class, true)); + \assert(\is_a($storyClass, StandaloneNestedStory::class, true)); - $metaData = new DocumentMetaData($data, $type); + $metaData = new StandaloneStoryMetaData($data, $type); $document = new $storyClass($metaData); return $this->mapDataToFields( @@ -116,14 +116,14 @@ public function hydrateBlok ( array $contentPath, string $type, array $data, - ) : StoryContent + ) : NestedStory { $definition = $this->componentManager->getDefinition($type); $blokClass = $definition->storyClass; - \assert(\is_a($blokClass, StoryContent::class, true)); + \assert(\is_a($blokClass, NestedStory::class, true)); - $metaData = new ContentMetaData( + $metaData = new NestedStoryMetaData( $data["_uid"], $data["component"], $data["_editable"], diff --git a/src/Manager/ComponentManager.php b/src/Manager/ComponentManager.php index 109c866..1e88c77 100644 --- a/src/Manager/ComponentManager.php +++ b/src/Manager/ComponentManager.php @@ -14,7 +14,7 @@ use Torr\Storyblok\Exception\Component\UnknownComponentKeyException; use Torr\Storyblok\Exception\Component\UnknownStoryTypeException; use Torr\Storyblok\Mapping\Storyblok; -use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\StandaloneNestedStory; /** * @final @@ -65,7 +65,7 @@ public function getAllComponents () : array /** * Returns the first component that creates a story of the given type * - * @template TStory of StoryDocument + * @template TStory of StandaloneNestedStory * * @param class-string $storyType * diff --git a/src/Story/Helper/ComponentPreviewData.php b/src/Story/Helper/ComponentPreviewData.php index 06dedaa..0047674 100644 --- a/src/Story/Helper/ComponentPreviewData.php +++ b/src/Story/Helper/ComponentPreviewData.php @@ -3,14 +3,14 @@ namespace Torr\Storyblok\Story\Helper; use Torr\Storyblok\Exception\Story\InvalidDataException; -use Torr\Storyblok\Story\MetaData\ContentMetaData; +use Torr\Storyblok\Story\MetaData\NestedStoryMetaData; final class ComponentPreviewData { /** * Normalizes the preview-related data to return it directly */ - public static function normalizeData (ContentMetaData $metaData) : array + public static function normalizeData (NestedStoryMetaData $metaData) : array { return [ "_uid" => $metaData->getUuid(), diff --git a/src/Story/MetaData/ContentMetaData.php b/src/Story/MetaData/NestedStoryMetaData.php similarity index 95% rename from src/Story/MetaData/ContentMetaData.php rename to src/Story/MetaData/NestedStoryMetaData.php index 7763f38..472986a 100644 --- a/src/Story/MetaData/ContentMetaData.php +++ b/src/Story/MetaData/NestedStoryMetaData.php @@ -2,7 +2,7 @@ namespace Torr\Storyblok\Story\MetaData; -class ContentMetaData +class NestedStoryMetaData { /** */ diff --git a/src/Story/MetaData/DocumentMetaData.php b/src/Story/MetaData/StandaloneStoryMetaData.php similarity index 98% rename from src/Story/MetaData/DocumentMetaData.php rename to src/Story/MetaData/StandaloneStoryMetaData.php index 5008570..6571a8d 100644 --- a/src/Story/MetaData/DocumentMetaData.php +++ b/src/Story/MetaData/StandaloneStoryMetaData.php @@ -5,7 +5,7 @@ use Torr\Storyblok\Exception\Story\StoryHydrationFailed; use Torr\Storyblok\Translation\LocaleHelper; -final class DocumentMetaData extends ContentMetaData +final class StandaloneStoryMetaData extends NestedStoryMetaData { private readonly array $data; private readonly array $slugSegments; diff --git a/src/Story/StoryContent.php b/src/Story/NestedStory.php similarity index 62% rename from src/Story/StoryContent.php rename to src/Story/NestedStory.php index a7fd5a2..f01767a 100644 --- a/src/Story/StoryContent.php +++ b/src/Story/NestedStory.php @@ -2,17 +2,17 @@ namespace Torr\Storyblok\Story; -use Torr\Storyblok\Story\MetaData\ContentMetaData; +use Torr\Storyblok\Story\MetaData\NestedStoryMetaData; /** * Base class for a nestable content element */ -abstract class StoryContent +abstract class NestedStory { /** */ public function __construct ( - protected readonly ContentMetaData $metaData, + protected readonly NestedStoryMetaData $metaData, ) {} @@ -27,7 +27,7 @@ public function getUuid () : string /** */ - public function getMetaData () : ContentMetaData + public function getMetaData () : NestedStoryMetaData { return $this->metaData; } diff --git a/src/Story/StoryDocument.php b/src/Story/StandaloneNestedStory.php similarity index 57% rename from src/Story/StoryDocument.php rename to src/Story/StandaloneNestedStory.php index 12cddd5..f9eeb9a 100644 --- a/src/Story/StoryDocument.php +++ b/src/Story/StandaloneNestedStory.php @@ -2,17 +2,17 @@ namespace Torr\Storyblok\Story; -use Torr\Storyblok\Story\MetaData\DocumentMetaData; +use Torr\Storyblok\Story\MetaData\StandaloneStoryMetaData; /** * Base class for a standalone storyblok component */ -abstract class StoryDocument extends StoryContent +abstract class StandaloneNestedStory extends NestedStory { /** */ public function __construct ( - DocumentMetaData $metaData, + StandaloneStoryMetaData $metaData, ) { parent::__construct($metaData); @@ -21,9 +21,9 @@ public function __construct ( /** */ - public function getMetaData () : DocumentMetaData + public function getMetaData () : StandaloneStoryMetaData { - \assert($this->metaData instanceof DocumentMetaData); + \assert($this->metaData instanceof StandaloneStoryMetaData); return $this->metaData; } diff --git a/src/TorrStoryblokBundle.php b/src/TorrStoryblokBundle.php index 7fe3271..6c69a27 100644 --- a/src/TorrStoryblokBundle.php +++ b/src/TorrStoryblokBundle.php @@ -10,7 +10,7 @@ use Torr\Storyblok\DependencyInjection\CollectComponentDefinitionsCompilerPass; use Torr\Storyblok\DependencyInjection\StoryblokBundleConfiguration; use Torr\Storyblok\DependencyInjection\StoryblokBundleExtension; -use Torr\Storyblok\Story\StoryDocument; +use Torr\Storyblok\Story\StandaloneNestedStory; final class TorrStoryblokBundle extends Bundle {