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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"php": "~8.3.0 || ~8.4.0",
"league/csv": "^9.27",
"nesbot/carbon": "^3.8.4",
"pimcore/static-resolver-bundle": "^3.5.0 ",
"pimcore/static-resolver-bundle": "^3.6.1 ",
"pimcore/generic-data-index-bundle": "^2.5.0",
"pimcore/pimcore": "^12.3",
"zircote/swagger-php": "^4.8 || ^5.0",
Expand Down
71 changes: 65 additions & 6 deletions doc/03_Extending/06_Data_Objects/01_Field_Definition_Adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ types. Each field type (input, date, relation, etc.) has an adapter that control
- **Saved** — transforming incoming API request data into the format the field definition
expects before it is stored.
- **Read** — normalizing stored data into an API-friendly response format.
- **Detail page** — providing a detail-page-specific representation that may differ from the
normalized value (falls back to normalization when not implemented).
- **Exported** — converting stored data into a string for grid/CSV export.
- **Inherited** — resolving inherited values in the data object hierarchy.
- **Previewed** — providing preview data for search results.
Expand Down Expand Up @@ -43,6 +45,7 @@ and can be added when your field type needs the corresponding capability.
|---|---|---|
| `SetterDataInterface` | **Yes** | Transform API request data for saving |
| `DataNormalizerInterface` | No | Customize the API response format |
| `DetailDataInterface` | No | Provide a detail-page-specific value (falls back to `DataNormalizerInterface`) |
| `DataExportInterface` | No | Provide a string representation for grid/CSV export |
| `DataInheritanceInterface` | No | Resolve inherited values in the object hierarchy |
| `SearchPreviewDataInterface` | No | Contribute preview data to search results |
Expand Down Expand Up @@ -128,6 +131,47 @@ Return the API-friendly representation of the value.

---

### DetailDataInterface

Implement this interface when the detail page of a data object should display a different
representation than the normalized value. When this interface
is **not** implemented, the system automatically falls back to
`DataNormalizerInterface::normalize()`.

**When to use:**

- The detail page requires a richer or more descriptive value than from `normalize()`.


```php
namespace Pimcore\Bundle\StudioBackendBundle\DataObject\Data;

use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\Model\FieldContextData;
use Pimcore\Model\DataObject\ClassDefinition\Data;
use Pimcore\Model\DataObject\Concrete;

interface DetailDataInterface
{
public function getDetailData(
Concrete $object,
mixed $value,
Data $fieldDefinition,
?FieldContextData $contextData = null,
): mixed;
}
```

| Parameter | Description |
|---|---|
| `$object` | The data object being loaded for the detail page. |
| `$value` | The raw stored value from the data object getter. |
| `$fieldDefinition` | The Pimcore field definition for this field. |
| `$contextData` | Container context (object brick, localized field, etc.), or `null` for top-level fields. |

Return the detail-page-specific representation of the value.

---

### DataExportInterface

Implement this interface when the field type needs custom handling for grid column display
Expand Down Expand Up @@ -314,6 +358,7 @@ namespace App\DataObject\Data\Adapter;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DataExportInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DataInheritanceInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DataNormalizerInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DetailDataInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\Model\FieldContextData;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\SearchPreviewDataInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\SetterDataInterface;
Expand All @@ -326,6 +371,7 @@ use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
final readonly class CustomFieldAdapter implements
SetterDataInterface,
DataNormalizerInterface,
DetailDataInterface,
DataExportInterface,
DataInheritanceInterface,
SearchPreviewDataInterface
Expand Down Expand Up @@ -374,6 +420,19 @@ final readonly class CustomFieldAdapter implements
return $value;
}

// --- DetailDataInterface (optional) ---

public function getDetailData(
Concrete $object,
mixed $value,
Data $fieldDefinition,
?FieldContextData $contextData = null,
): mixed {
// Return a detail-page-specific representation of the value.
// When this interface is not implemented, normalize() is called instead.
return $value;
}

// --- DataExportInterface (optional) ---

public function getExportData(
Expand Down Expand Up @@ -466,19 +525,19 @@ class required.
| `InputQuantityValueAdapter` | `inputQuantityValue` | Setter |
| `QuantityValueRangeAdapter` | `quantityValueRange` | Setter |
| `UrlSlugAdapter` | `urlSlug` | Setter |
| `CalculatedValueAdapter` | `calculatedValue` | Setter |
| `CalculatedValueAdapter` | `calculatedValue` | Setter, Detail |
| `EncryptedFieldAdapter` | `encryptedField` | Setter, Normalizer, Export |
| `TableAdapter` | `table` | Setter, SearchPreview |
| `StructuredTableAdapter` | `structuredTable` | Setter, Normalizer, SearchPreview |
| `BlockAdapter` | `block` | Setter, Normalizer, SearchPreview |
| `FieldCollectionsAdapter` | `fieldcollections` | Setter, Normalizer, SearchPreview |
| `ObjectBricksAdapter` | `objectbricks` | Setter, Normalizer, Inheritance, SearchPreview |
| `LocalizedFieldsAdapter` | `localizedfields` | Setter, Normalizer, Inheritance, SearchPreview |
| `ClassificationStoreAdapter` | `classificationstore` | Setter, Normalizer, Inheritance, SearchPreview |
| `ObjectBricksAdapter` | `objectbricks` | Setter, Normalizer, Detail, Inheritance, SearchPreview |
| `LocalizedFieldsAdapter` | `localizedfields` | Setter, Normalizer, Detail, Inheritance, SearchPreview |
| `ClassificationStoreAdapter` | `classificationstore` | Setter, Normalizer, Detail, Inheritance, SearchPreview |

**Legend:** Setter = `SetterDataInterface`, Normalizer = `DataNormalizerInterface`,
Export = `DataExportInterface`, Inheritance = `DataInheritanceInterface`,
SearchPreview = `SearchPreviewDataInterface`
Detail = `DetailDataInterface`, Export = `DataExportInterface`,
Inheritance = `DataInheritanceInterface`, SearchPreview = `SearchPreviewDataInterface`

:::info

Expand Down
88 changes: 87 additions & 1 deletion src/DataObject/Data/Adapter/CalculatedValueAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,38 @@

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

use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DetailDataInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\Model\FieldContextData;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\SetterDataInterface;
use Pimcore\Bundle\StaticResolverBundle\Models\DataObject\DataObjectServiceResolverInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Service\DataAdapterLoaderInterface;
use Pimcore\Model\DataObject\ClassDefinition\Data;
use Pimcore\Model\DataObject\ClassDefinition\Data\CalculatedValue as CalculatedValueDefinition;
use Pimcore\Model\DataObject\Concrete;
use Pimcore\Model\DataObject\Data\CalculatedValue;
use Pimcore\Model\DataObject\Objectbrick\Data\AbstractData;
use Pimcore\Model\UserInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

/**
* @internal
*/
#[AutoconfigureTag(DataAdapterLoaderInterface::ADAPTER_TAG)]
final readonly class CalculatedValueAdapter implements SetterDataInterface
final readonly class CalculatedValueAdapter implements SetterDataInterface, DetailDataInterface
{
private const string OWNER_TYPE_OBJECT = 'object';

private const string OWNER_TYPE_LOCALIZED_FIELD = 'localizedfield';

private const string OWNER_TYPE_OBJECT_BRICK = 'objectbrick';

private const string LOCALIZED_FIELDS_NAME = 'localizedfields';

public function __construct(
private DataObjectServiceResolverInterface $dataObjectServiceResolver,
) {
}

public function getDataForSetter(
Concrete $element,
Data $fieldDefinition,
Expand All @@ -38,4 +56,72 @@ public function getDataForSetter(
): null {
return null;
}

public function getDetailData(
Concrete $object,
mixed $value,
Data $fieldDefinition,
?FieldContextData $contextData = null,
): ?string {
if (!$fieldDefinition instanceof CalculatedValueDefinition) {
return null;
}

$calculatedValue = new CalculatedValue($fieldDefinition->getName());
$this->applyContextualData($calculatedValue, $fieldDefinition, $contextData);

return $this->dataObjectServiceResolver->getCalculatedFieldValueForEditMode(
$object,
[],
$calculatedValue,
);
}

private function applyContextualData(
CalculatedValue $calculatedValue,
CalculatedValueDefinition $fieldDefinition,
?FieldContextData $contextData,
): void {
$contextObject = $contextData?->getContextObject();

if ($contextObject instanceof AbstractData) {
$calculatedValue->setContextualData(
self::OWNER_TYPE_OBJECT_BRICK,
$contextObject->getFieldname(),
$contextObject->getType(),
$fieldDefinition->getName(),
null,
null,
$fieldDefinition,
);

return;
}

$language = $contextData?->getLanguage();

if ($language !== null) {
$calculatedValue->setContextualData(
self::OWNER_TYPE_LOCALIZED_FIELD,
self::LOCALIZED_FIELDS_NAME,
null,
$language,
null,
null,
$fieldDefinition,
);

return;
}

$calculatedValue->setContextualData(
self::OWNER_TYPE_OBJECT,
$fieldDefinition->getName(),
null,
null,
null,
null,
$fieldDefinition,
);
}
}
74 changes: 69 additions & 5 deletions src/DataObject/Data/Adapter/ClassificationStoreAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
use Pimcore\Bundle\StaticResolverBundle\Models\DataObject\ClassificationStore\ServiceResolverInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DataInheritanceInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DataNormalizerInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\DetailDataInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\Model\FieldContextData;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\Model\InheritanceData;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\SearchPreviewDataInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Data\SetterDataInterface;
use Pimcore\Bundle\StaticResolverBundle\Models\DataObject\DataObjectServiceResolverInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Service\DataAdapterLoaderInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Service\DataAdapterServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\DataObject\Service\DataServiceInterface;
Expand All @@ -40,9 +42,11 @@
use Pimcore\Model\DataObject\Classificationstore;
use Pimcore\Model\DataObject\Classificationstore as ClassificationstoreModel;
use Pimcore\Model\DataObject\Classificationstore\GroupConfig;
use Pimcore\Model\DataObject\Classificationstore\KeyConfig;
use Pimcore\Model\DataObject\Classificationstore\KeyGroupRelation;
use Pimcore\Model\DataObject\Classificationstore\KeyGroupRelation\Listing as KeyGroupRelationListing;
use Pimcore\Model\DataObject\Concrete;
use Pimcore\Model\DataObject\Data\CalculatedValue;
use Pimcore\Model\UserInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use function in_array;
Expand All @@ -54,12 +58,14 @@
final readonly class ClassificationStoreAdapter implements
SetterDataInterface,
DataNormalizerInterface,
DetailDataInterface,
DataInheritanceInterface,
SearchPreviewDataInterface
{
use ValidateObjectDataTrait;

public function __construct(
private DataObjectServiceResolverInterface $dataObjectServiceResolver,
private DefinitionCacheResolverInterface $definitionCacheResolver,
private DataAdapterServiceInterface $dataAdapterService,
private DataServiceInterface $dataService,
Expand All @@ -68,7 +74,7 @@ public function __construct(
private LanguageServiceInterface $languageService,
private ServiceResolverInterface $serviceResolver,
private SecurityServiceInterface $securityService,
private ToolResolverInterface $toolResolver
private ToolResolverInterface $toolResolver,
) {
}

Expand Down Expand Up @@ -116,6 +122,15 @@ public function getDataForSetter(
public function normalize(
mixed $value,
Data $fieldDefinition
): ?array {
return $this->handleNormalize($value, $fieldDefinition, useComputedValues: true);
}

public function getDetailData(
Concrete $object,
mixed $value,
Data $fieldDefinition,
?FieldContextData $contextData = null,
): ?array {
return $this->handleNormalize($value, $fieldDefinition);
}
Expand Down Expand Up @@ -192,7 +207,8 @@ public function getPreviewFieldData(
private function handleNormalize(
mixed $value,
Data $fieldDefinition,
?UserInterface $user = null
?UserInterface $user = null,
bool $useComputedValues = false,
): ?array {
if (!$value instanceof ClassificationstoreModel ||
!$fieldDefinition instanceof ClassificationstoreDefinition
Expand All @@ -216,7 +232,13 @@ private function handleNormalize(
$keys = $this->getClassificationStoreKeysFromGroup($groupId);
foreach ($validLanguages as $validLanguage) {
foreach ($keys as $key) {
$normalizedValue = $this->getNormalizedValue($value, $groupId, $key, $validLanguage);
$normalizedValue = $this->getResolvedValue(
$value,
$groupId,
$key,
$validLanguage,
$useComputedValues,
);

if ($normalizedValue !== null) {
$resultItems[$groupId][$validLanguage][$key->getKeyId()] = $normalizedValue;
Expand Down Expand Up @@ -440,15 +462,57 @@ private function getClassificationStoreKeysFromGroup(int $groupId): array
/**
* @throws DatabaseException
*/
private function getNormalizedValue(
private function getResolvedValue(
ClassificationstoreModel $classificationstore,
int $groupId,
KeyGroupRelation $key,
string $language
string $language,
bool $useComputedValue = false,
): mixed {
$keyConfig = $this->definitionCacheResolver->get($key->getKeyId());
if ($keyConfig === null) {
return null;
}

if ($useComputedValue && $keyConfig->getType() === 'calculatedValue') {
return $this->resolveComputedValue($classificationstore, $groupId, $key, $language, $keyConfig);
}

return $this->getValue($classificationstore, $groupId, $key, $language);
}

/**
* @throws DatabaseException
*/
private function resolveComputedValue(
ClassificationstoreModel $classificationstore,
int $groupId,
KeyGroupRelation $key,
string $language,
KeyConfig $keyConfig,
): mixed {
$childDef = $this->serviceResolver->getFieldDefinitionFromKeyConfig($keyConfig);
if ($childDef === null) {
return null;
}

$calculatedValue = new CalculatedValue($classificationstore->getFieldname());
$calculatedValue->setContextualData(
'classificationstore',
$classificationstore->getFieldname(),
null,
$language,
$groupId,
$key->getKeyId(),
$childDef,
);

return $this->dataObjectServiceResolver->getCalculatedFieldValue(
$classificationstore->getObject(),
$calculatedValue,
);
}

/**
* @throws DatabaseException
*/
Expand Down
Loading
Loading