Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions config/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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: ~
8 changes: 7 additions & 1 deletion config/pimcore/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
110 changes: 110 additions & 0 deletions doc/10_Extending_Studio/01_Assets/01_Metadata_adapters.md
Original file line number Diff line number Diff line change
@@ -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
<?php
declare(strict_types=1);

namespace App\Adapter;

use Pimcore\Bundle\StaticResolverBundle\Models\Element\ServiceResolverInterface;
use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\DataDenormalizerInterface;
use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\DataNormalizerInterface;
use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\MetaDataAdapterInterface;
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\AdapterLoader;
use Pimcore\Model\DataObject\MyCustomObject;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

// Each metadata adapter must be tagged with the `pimcore.studio_backend.metadata_adapter` tag
// It is possible to use the `AdapterLoader::METADATA_ADAPTER_TAG` enum for this purpose
// The adapter must implement at least the `MetaDataAdapterInterface` interface in order to be recognized by the system
#[AutoconfigureTag(AdapterLoader::METADATA_ADAPTER_TAG->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.

:::
38 changes: 38 additions & 0 deletions src/Metadata/Data/Adapter/BooleanAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Metadata\Data\Adapter;

use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\DataNormalizerInterface;
use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\MetaDataAdapterInterface;
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\AdapterLoader;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

/**
* @internal
*/
#[AutoconfigureTag(AdapterLoader::METADATA_ADAPTER_TAG->value)]
final readonly class BooleanAdapter implements MetaDataAdapterInterface, DataNormalizerInterface
{
public function normalize(mixed $value, string $type): ?bool
{
if ($value === null) {
return null;
}

return (bool)$value;
}
}
83 changes: 83 additions & 0 deletions src/Metadata/Data/Adapter/ManyToOneRelationAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Metadata\Data\Adapter;

use Pimcore\Bundle\StaticResolverBundle\Models\Element\ServiceResolverInterface;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException;
use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\DataDenormalizerInterface;
use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\DataNormalizerInterface;
use Pimcore\Bundle\StudioBackendBundle\Metadata\Data\MetaDataAdapterInterface;
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\AdapterLoader;
use Pimcore\Bundle\StudioBackendBundle\Util\Trait\ElementProviderTrait;
use Pimcore\Model\DataObject\Concrete;
use Pimcore\Model\Document;
use Pimcore\Model\Element\ElementInterface;
use Pimcore\Model\UserInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use function is_array;

/**
* @internal
*/
#[AutoconfigureTag(AdapterLoader::METADATA_ADAPTER_TAG->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;
}
}
}
7 changes: 4 additions & 3 deletions src/Metadata/Data/DataDenormalizerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
21 changes: 16 additions & 5 deletions src/Metadata/Patcher/Adapter/CustomMetadataAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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,
];
}

Expand Down
2 changes: 0 additions & 2 deletions src/Metadata/Service/DataAdapterLoaderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
*/
interface DataAdapterLoaderInterface
{
public const string ADAPTER_TAG = 'pimcore.studio_backend.metadata_adapter';

/**
* @throws InvalidArgumentException
*/
Expand Down
7 changes: 6 additions & 1 deletion src/Metadata/Service/DataResolverServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading
Loading