diff --git a/config/metadata.yaml b/config/metadata.yaml index 3e46120c1..c366272c5 100644 --- a/config/metadata.yaml +++ b/config/metadata.yaml @@ -12,7 +12,10 @@ services: public: true tags: [ 'controller.service_arguments' ] + # # Service + # + Pimcore\Bundle\StudioBackendBundle\Metadata\Service\MetadataServiceInterface: class: Pimcore\Bundle\StudioBackendBundle\Metadata\Service\MetadataService @@ -27,17 +30,34 @@ services: arguments: $loader: '@pimcore.implementation_loader.asset.metadata.data' + # # Hydrator + # + Pimcore\Bundle\StudioBackendBundle\Metadata\Hydrator\MetadataHydratorInterface: class: Pimcore\Bundle\StudioBackendBundle\Metadata\Hydrator\MetadataHydrator + # # Repository + # + Pimcore\Bundle\StudioBackendBundle\Metadata\Repository\MetadataRepositoryInterface: class: Pimcore\Bundle\StudioBackendBundle\Metadata\Repository\MetadataRepository Pimcore\Bundle\StudioBackendBundle\Metadata\Updater\Adapter\CustomMetadataAdapter: tags: [ 'pimcore.studio_backend.update_adapter' ] + # # Patcher + # + Pimcore\Bundle\StudioBackendBundle\Metadata\Patcher\Adapter\CustomMetadataAdapter: tags: [ 'pimcore.studio_backend.patch_adapter' ] + + # + # Data Adapters + # + + Pimcore\Bundle\StudioBackendBundle\Metadata\Data\Adapter\BooleanAdapter: ~ + + Pimcore\Bundle\StudioBackendBundle\Metadata\Data\Adapter\ManyToOneRelationAdapter: ~ diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml index 462519fc3..fd1b9bbe7 100644 --- a/config/pimcore/config.yaml +++ b/config/pimcore/config.yaml @@ -99,7 +99,13 @@ pimcore_studio_backend: document: ['content', 'seo', 'warning', 'notice'] data-object: ['content', 'seo', 'warning', 'notice'] - asset_metadata_adapter_mapping: [] + asset_metadata_adapter_mapping: + Pimcore\Bundle\StudioBackendBundle\Metadata\Data\Adapter\BooleanAdapter: + - "checkbox" + Pimcore\Bundle\StudioBackendBundle\Metadata\Data\Adapter\ManyToOneRelationAdapter: + - "asset" + - "document" + - "object" data_object_data_adapter_mapping: Pimcore\Bundle\StudioBackendBundle\DataObject\Data\Adapter\AdvancedManyToManyRelationAdapter: diff --git a/doc/10_Extending_Studio/01_Assets/01_Metadata_adapters.md b/doc/10_Extending_Studio/01_Assets/01_Metadata_adapters.md new file mode 100644 index 000000000..5d85eb61d --- /dev/null +++ b/doc/10_Extending_Studio/01_Assets/01_Metadata_adapters.md @@ -0,0 +1,110 @@ +# Extending metadata adapters + +Asset metadata adapters are used to process metadata values before they are e.g. saved to the database or displayed in the user interface. + +Each metadata field is mapped to the corresponding adapter by its type. This allows you to modify the metadata values in a flexible way. + +## How to add a custom metadata adapters + +In case of custom metadata types, it is possible to add custom adapters for their processing. + +The following example shows how to implement a custom adapter for the `myCustom` metadata type. + +### 1. Register your adapter + +```yaml +services: + App\Adapter\MyCustomAdapter: ~ +``` + +### 2. Implement your adapter + +```php +value)] +final readonly class MyCustomAdapter implements + MetaDataAdapterInterface, + DataNormalizerInterface, + DataDenormalizerInterface +{ + + public function __construct( + private ServiceResolverInterface $serviceResolver + ) { + } + + // Let's assume that the `myCustom` metadata value is returning a specific object. + // However, we want to return just some specific properties of this object. + // We can therefore normalize the value to an array and return only the required properties. + public function normalize(mixed $value, string $type): ?int + { + if (!$value instanceof MyCustomObject) { + return null; + } + + return [ + 'id' => $value->getId(), + 'name' => $value->getName(), + 'key' => $value->getKey(), + 'someImportantValue' => $value->getSomeImportantValue(), + ]; + } + + // In the denormalize method, we can convert the normalized array back to the original object + public function denormalize( + array $customMetadata, + UserInterface $user, + array $existingMetadata = [], + bool $isPatch = false + ): ?ElementInterface + { + $value = $customMetadata['data'] ?? null; + if (!is_array($value) || empty($value['id'])) { + return null; + } + + $element = $this->serviceResolver->getElementById('object', $value['id']); + if (!$element instanceof MyCustomObject) { + return null; + } + + return $element; + } +} +``` + +### 3. Add the mapping of the metadata type and the new adapter + +```yaml +pimcore_studio_backend: + asset_metadata_adapter_mapping: + App\Adapter\MyCustomAdapter: # The adapter class that should be used for processing the metadata + - "myCustom" # The metadata type that should be processed by the adapter + +``` + +Important interfaces: +- `Pimcore\Bundle\StudioBackendBundle\Metadata\Data\MetaDataAdapterInterface` - The main marker interface that must be implemented by the adapter. +- `Pimcore\Bundle\StudioBackendBundle\Metadata\Data\DataNormalizerInterface` - The interface that needs to be implemented if the adapter should be able to normalize the metadata value. +- `Pimcore\Bundle\StudioBackendBundle\Metadata\Data\DataDenormalizerInterface` - The interface that needs to be implemented if the adapter should be able to denormalize the metadata value. + +:::info + +Each adapter has to be tagged with the `pimcore.studio_backend.metadata_adapter` tag in order to be recognized by the system. + +::: \ No newline at end of file diff --git a/src/Metadata/Data/Adapter/BooleanAdapter.php b/src/Metadata/Data/Adapter/BooleanAdapter.php new file mode 100644 index 000000000..49d628bd1 --- /dev/null +++ b/src/Metadata/Data/Adapter/BooleanAdapter.php @@ -0,0 +1,38 @@ +value)] +final readonly class BooleanAdapter implements MetaDataAdapterInterface, DataNormalizerInterface +{ + public function normalize(mixed $value, string $type): ?bool + { + if ($value === null) { + return null; + } + + return (bool)$value; + } +} diff --git a/src/Metadata/Data/Adapter/ManyToOneRelationAdapter.php b/src/Metadata/Data/Adapter/ManyToOneRelationAdapter.php new file mode 100644 index 000000000..c091196a6 --- /dev/null +++ b/src/Metadata/Data/Adapter/ManyToOneRelationAdapter.php @@ -0,0 +1,83 @@ +value)] +final readonly class ManyToOneRelationAdapter implements + MetaDataAdapterInterface, + DataNormalizerInterface, + DataDenormalizerInterface +{ + use ElementProviderTrait; + + public function __construct( + private ServiceResolverInterface $serviceResolver + ) { + } + + public function normalize(mixed $value, string $type): ?array + { + if ($value === null) { + return null; + } + + return [ + 'id' => $value->getId(), + 'type' => $value->getType(), + 'fullPath' => $value->getRealFullPath(), + 'subtype' => $value->getType(), + 'isPublished' => ($value instanceof Concrete || $value instanceof Document) ? + $value->isPublished() : + null, + ]; + } + + public function denormalize( + array $customMetadata, + UserInterface $user, + array $existingMetadata = [], + bool $isPatch = false + ): ?ElementInterface { + $value = $customMetadata['data'] ?? null; + if (!is_array($value) || empty($value['id'])) { + return null; + } + + try { + return $this->getElement($this->serviceResolver, $value['type'], $value['id']); + } catch (NotFoundException) { + return null; + } + } +} diff --git a/src/Metadata/Data/DataDenormalizerInterface.php b/src/Metadata/Data/DataDenormalizerInterface.php index 186e54db8..7c0e7fcd7 100644 --- a/src/Metadata/Data/DataDenormalizerInterface.php +++ b/src/Metadata/Data/DataDenormalizerInterface.php @@ -21,8 +21,9 @@ interface DataDenormalizerInterface { public function denormalize( - mixed $value, - string $type, - UserInterface $user + array $customMetadata, + UserInterface $user, + array $existingMetadata = [], + bool $isPatch = false ): mixed; } diff --git a/src/Metadata/Patcher/Adapter/CustomMetadataAdapter.php b/src/Metadata/Patcher/Adapter/CustomMetadataAdapter.php index 097d4c3b7..188165290 100644 --- a/src/Metadata/Patcher/Adapter/CustomMetadataAdapter.php +++ b/src/Metadata/Patcher/Adapter/CustomMetadataAdapter.php @@ -73,7 +73,12 @@ public function patch(ElementInterface $element, array $data, UserInterface $use foreach (self::PATCHABLE_KEYS as $patchKey) { if (array_key_exists($patchKey, $metadataForPatch[$index])) { - $metadata[$patchKey] = $this->getExistingEntryValue($metadataForPatch[$index], $patchKey, $user); + $metadata[$patchKey] = $this->getExistingEntryValue( + $metadataForPatch[$index], + $metadata, + $patchKey, + $user + ); } } $patchedMetadata[] = $metadata; @@ -107,13 +112,17 @@ public function supportedElementTypes(): array ]; } - private function getExistingEntryValue(array $metadata, string $key, UserInterface $user): mixed - { + private function getExistingEntryValue( + array $metadata, + array $existingMetadata, + string $key, + UserInterface $user + ): mixed { if ($key !== 'data') { return $metadata[$key]; } - return $this->dataResolverService->denormalizeData($metadata, $user); + return $this->dataResolverService->denormalizeData($metadata, $user, $existingMetadata, true); } /** @@ -139,7 +148,9 @@ private function processNewMetadataEntry(array $metadata, UserInterface $user): 'name' => $predefined->getName(), 'language' => $metadata['language'] ?? '', 'type' => $predefined->getType(), - 'data' => $metadata['data'] ? $this->dataResolverService->denormalizeData($metadata, $user) : null, + 'data' => $metadata['data'] ? + $this->dataResolverService->denormalizeData($metadata, $user, [], true) : + null, ]; } diff --git a/src/Metadata/Service/DataAdapterLoaderInterface.php b/src/Metadata/Service/DataAdapterLoaderInterface.php index ddae3b8a7..5fde76e55 100644 --- a/src/Metadata/Service/DataAdapterLoaderInterface.php +++ b/src/Metadata/Service/DataAdapterLoaderInterface.php @@ -24,8 +24,6 @@ */ interface DataAdapterLoaderInterface { - public const string ADAPTER_TAG = 'pimcore.studio_backend.metadata_adapter'; - /** * @throws InvalidArgumentException */ diff --git a/src/Metadata/Service/DataResolverServiceInterface.php b/src/Metadata/Service/DataResolverServiceInterface.php index 492cd170a..8215ed158 100644 --- a/src/Metadata/Service/DataResolverServiceInterface.php +++ b/src/Metadata/Service/DataResolverServiceInterface.php @@ -32,5 +32,10 @@ public function normalizeData(array $customMetadata): mixed; /** * @throws InvalidArgumentException */ - public function denormalizeData(array $customMetadata, UserInterface $user): mixed; + public function denormalizeData( + array $customMetadata, + UserInterface $user, + array $existingMetadata = [], + bool $isPatch = false + ): mixed; } diff --git a/src/Metadata/Service/DataResolverServiceService.php b/src/Metadata/Service/DataResolverServiceService.php index 2263e53d7..e9878c80a 100644 --- a/src/Metadata/Service/DataResolverServiceService.php +++ b/src/Metadata/Service/DataResolverServiceService.php @@ -48,17 +48,22 @@ public function normalizeData(array $customMetadata): mixed /** * {@inheritdoc} */ - public function denormalizeData(array $customMetadata, UserInterface $user): mixed - { + public function denormalizeData( + array $customMetadata, + UserInterface $user, + array $existingMetadata = [], + bool $isPatch = false + ): mixed { $adapter = $this->dataAdapterService->getDenormalizerAdapter($customMetadata['type']); + $data = $customMetadata['data']; if ($adapter === null) { - return $customMetadata['data']; + return $data; } if ($adapter instanceof NormalizerInterface) { - return $adapter->denormalize($customMetadata['data']); + return $adapter->denormalize($data); } - return $adapter->denormalize($customMetadata['data'], $customMetadata['type'], $user); + return $adapter->denormalize($customMetadata, $user, $existingMetadata, $isPatch); } } diff --git a/src/Metadata/Service/Loader/TaggedIteratorMetadataAdapter.php b/src/Metadata/Service/Loader/TaggedIteratorMetadataAdapter.php index 26c6b50e6..ef7ac41f3 100644 --- a/src/Metadata/Service/Loader/TaggedIteratorMetadataAdapter.php +++ b/src/Metadata/Service/Loader/TaggedIteratorMetadataAdapter.php @@ -19,6 +19,7 @@ use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\MetaDataAdapterInterface; use Pimcore\Bundle\StudioBackendBundle\Metadata\Service\DataAdapterLoaderInterface; +use Pimcore\Bundle\StudioBackendBundle\Util\Constant\AdapterLoader; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use function get_class; use function sprintf; @@ -29,7 +30,7 @@ final readonly class TaggedIteratorMetadataAdapter implements DataAdapterLoaderInterface { public function __construct( - #[TaggedIterator(self::ADAPTER_TAG)] + #[TaggedIterator(AdapterLoader::METADATA_ADAPTER_TAG->value)] private iterable $taggedAdapter, ) { } diff --git a/src/Util/Constant/AdapterLoader.php b/src/Util/Constant/AdapterLoader.php new file mode 100644 index 000000000..f7eab4f64 --- /dev/null +++ b/src/Util/Constant/AdapterLoader.php @@ -0,0 +1,22 @@ +