-
-
Notifications
You must be signed in to change notification settings - Fork 6
Feature/extensible metadata properties #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/extensible-metadata-properties
Are you sure you want to change the base?
Changes from all commits
6f06c16
8d4df59
e902ffb
8d03077
d81f183
c4e076a
46c2194
86a1684
adf9ad7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Neos\MetaData\Command; | ||
|
|
||
| use Neos\Flow\Cli\CommandController; | ||
| use Neos\Media\Domain\Model\Asset; | ||
| use Neos\Media\Domain\Repository\AssetRepository; | ||
| use Neos\MetaData\Domain\Dto\MetaDataAssetReference; | ||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyName; | ||
| use Neos\MetaData\MetaDataManager; | ||
|
|
||
| final class AssetMetaDataMigrationCommandController extends CommandController | ||
| { | ||
| public function __construct( | ||
| private readonly MetaDataManager $metaDataManager, | ||
| private readonly AssetRepository $assetRepository, | ||
| ) { | ||
| parent::__construct(); | ||
| } | ||
|
|
||
| public function migrateExistingAssetPropertiesCommand(): void | ||
| { | ||
| foreach ($this->assetRepository->findAll() as $asset) { | ||
| /** @var Asset $asset */ | ||
| $title = $asset->getTitle(); | ||
| $caption = $asset->getCaption(); | ||
| $copyrightNotice = $asset->getCopyrightNotice(); | ||
| $metaDataAssetReference = MetaDataAssetReference::fromAsset($asset); | ||
|
|
||
| if (!empty($caption)) { | ||
| $this->metaDataManager->setMetaDataPropertyValue( | ||
| $metaDataAssetReference, | ||
| MetaDataPropertyName::fromString('caption'), | ||
| $asset->getCaption(), | ||
| ); | ||
| } | ||
| if (!empty($copyrightNotice)) { | ||
| $this->metaDataManager->setMetaDataPropertyValue( | ||
| $metaDataAssetReference, | ||
| MetaDataPropertyName::fromString('copyright'), | ||
| $asset->getCopyrightNotice(), | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Neos\MetaData\Configuration; | ||
|
|
||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyDefinitions; | ||
|
|
||
| /** | ||
| * Provider for global property configuration (usually from settings (YAML)) | ||
| */ | ||
| interface MetaDataConfigurationProvider | ||
| { | ||
| public function getPropertyConfiguration(): MetaDataPropertyDefinitions; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| <?php | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Neos\MetaData\Configuration; | ||
|
|
||
| use Neos\Flow\I18n\Translator; | ||
| use Neos\MetaData\Domain\Dto\MetaDataEditorDefinition; | ||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyDefinition; | ||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyDefinitions; | ||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyName; | ||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyType; | ||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyUiDefinition; | ||
|
|
||
| class MetaDataConfigurationProviderYamlAdapter implements MetaDataConfigurationProvider | ||
| { | ||
| public function __construct( | ||
| private readonly array $propertyConfiguration, | ||
| private readonly Translator $translator, | ||
| ) | ||
| { | ||
| } | ||
|
|
||
| public function getPropertyConfiguration(): MetaDataPropertyDefinitions | ||
| { | ||
| $propertyDefinitions = []; | ||
| foreach ($this->propertyConfiguration as $propertyName => $propertyDefinition) { | ||
| $propertyDefinitions[] = new MetaDataPropertyDefinition( | ||
| MetaDataPropertyName::fromString($propertyName), | ||
| match ($propertyDefinition['type'] ?? null) { | ||
| 'integer' => MetaDataPropertyType::integer, | ||
| 'boolean' => MetaDataPropertyType::boolean, | ||
| default => MetaDataPropertyType::string, | ||
| }, | ||
| $propertyDefinition['globalScope'] ?? false, | ||
| new MetaDataPropertyUiDefinition( | ||
| $this->translatePropertyName($propertyName, $propertyDefinition['ui']['label']), | ||
| MetaDataEditorDefinition::create( | ||
| editorType: $propertyDefinition['ui']['editor'] ?? null, | ||
| options: $propertyDefinition['ui']['editorOptions'] ?? [], | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
| return MetaDataPropertyDefinitions::create(...$propertyDefinitions); | ||
| } | ||
|
|
||
| // ----------------------- | ||
|
|
||
| private function translatePropertyName(string $propertyName, ?string $label): string | ||
| { | ||
| if ($label === 'i18n') { | ||
| $translationShortHandString = sprintf('Neos.MetaData.Main.properties.%s', $propertyName); | ||
| return $this->translator->translateById($translationShortHandString, [], null, null, 'Main', 'Neos.MetaData') ?? $propertyName; | ||
| } elseif ($label !== null) { | ||
| return $label; | ||
| } | ||
| return $propertyName; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?php | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Neos\MetaData\Configuration; | ||
|
|
||
| use Neos\Flow\I18n\Translator; | ||
|
|
||
| class MetaDataConfigurationProviderYamlAdapterFactory | ||
| { | ||
| public function __construct( | ||
| private readonly array $propertyConfiguration, | ||
| private readonly Translator $translator, | ||
| ) | ||
| { | ||
| } | ||
|
|
||
| public function create(): MetaDataConfigurationProvider | ||
| { | ||
| return new MetaDataConfigurationProviderYamlAdapter($this->propertyConfiguration, $this->translator); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| <?php | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Neos\MetaData\DimensionSpacePointProvider; | ||
|
|
||
| use Neos\ContentRepository\Domain\Service\ConfigurationContentDimensionPresetSource; | ||
| use Neos\MetaData\Domain\Dto\MetaDataDimensionSpacePoint; | ||
| use Neos\MetaData\Domain\Dto\MetaDataDimensionSpacePoints; | ||
|
|
||
| class DimensionSpacePointProviderContentRepositoryAdapter implements DimensionSpacePointProvider | ||
| { | ||
| private ?array $allPresets = null; | ||
|
|
||
| public function __construct( | ||
| private readonly ConfigurationContentDimensionPresetSource $configurationContentDimensionPresetSource, | ||
| ) | ||
| { | ||
| } | ||
|
|
||
| private function getAllPresets(): array | ||
| { | ||
| if ($this->allPresets === null) { | ||
| $this->allPresets = $this->configurationContentDimensionPresetSource->getAllPresets(); | ||
| } | ||
| return $this->allPresets; | ||
| } | ||
|
|
||
| public function getDefaultDimensionSpacePoint(): MetaDataDimensionSpacePoint | ||
| { | ||
| return MetaDataDimensionSpacePoint::fromCoordinates([]); | ||
| } | ||
|
|
||
| public function getDimensionSpacePointChain(?MetaDataDimensionSpacePoint $dimensionSpacePoint = null): MetaDataDimensionSpacePoints | ||
| { | ||
| if ($dimensionSpacePoint === null) { | ||
| $dimensionSpacePoint = $this->getDefaultDimensionSpacePoint(); | ||
| } | ||
|
|
||
| if ($dimensionSpacePoint->coordinates === []) { | ||
| return MetaDataDimensionSpacePoints::create($dimensionSpacePoint); | ||
| } | ||
|
|
||
| // For each coordinate, resolve the ordered fallback chain from the matching preset | ||
| $perDimensionChains = []; | ||
| foreach ($dimensionSpacePoint->coordinates as $dimensionName => $primaryValue) { | ||
| $chain = [$primaryValue]; // safe default: just the value itself | ||
| foreach ($this->getAllPresets()[$dimensionName]['presets'] ?? [] as $preset) { | ||
| if (($preset['values'][0] ?? null) === $primaryValue) { | ||
| $chain = $preset['values']; | ||
| break; | ||
| } | ||
| } | ||
| $perDimensionChains[$dimensionName] = $chain; | ||
| } | ||
|
|
||
| // Build Cartesian product of all per-dimension chains, tracking fallback distance per combo | ||
| $combos = [['coords' => [], 'distance' => 0]]; | ||
| foreach ($perDimensionChains as $dimensionName => $chain) { | ||
| $expanded = []; | ||
| foreach ($combos as $combo) { | ||
| foreach ($chain as $index => $value) { | ||
| $expanded[] = [ | ||
| 'coords' => array_merge($combo['coords'], [$dimensionName => $value]), | ||
| 'distance' => $combo['distance'] + $index, | ||
| ]; | ||
| } | ||
| } | ||
| $combos = $expanded; | ||
| } | ||
|
|
||
| // Sort by total fallback distance: most specific (0) first | ||
| usort($combos, fn($a, $b) => $a['distance'] <=> $b['distance']); | ||
|
|
||
| $spacePoints = array_map( | ||
| fn($combo) => MetaDataDimensionSpacePoint::fromCoordinates($combo['coords']), | ||
| $combos | ||
| ); | ||
|
|
||
| $spacePoints[] = $this->getDefaultDimensionSpacePoint(); | ||
| return MetaDataDimensionSpacePoints::create(...$spacePoints); | ||
| } | ||
|
|
||
| public function isDimensionSpacePointValid(MetaDataDimensionSpacePoint $dimensionSpacePoint): bool | ||
| { | ||
| if (empty($this->getAllPresets())) { | ||
| return $dimensionSpacePoint->coordinates === []; | ||
| } | ||
|
|
||
| if (count($dimensionSpacePoint->coordinates) !== count($this->getAllPresets())) { | ||
| return false; | ||
| } | ||
|
|
||
| $presetIdentifiers = []; | ||
| foreach ($dimensionSpacePoint->coordinates as $dimensionName => $value) { | ||
| foreach ($this->getAllPresets()[$dimensionName]['presets'] ?? [] as $presetIdentifier => $preset) { | ||
| if ($preset['values'][0] === $value) { | ||
| $presetIdentifiers[$dimensionName] = $presetIdentifier; | ||
| break; | ||
| } | ||
| } | ||
| if (!isset($presetIdentifiers[$dimensionName])) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return $this->configurationContentDimensionPresetSource->isPresetCombinationAllowedByConstraints($presetIdentifiers); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,8 +36,25 @@ public function include(MetaDataDimensionSpacePoint $dimensionSpacePoint): bool | |
| return false; | ||
| } | ||
|
|
||
| public function getByHash(string $hash): MetaDataDimensionSpacePoint|null | ||
| { | ||
| foreach ($this->spacePoints as $spacePoint) { | ||
| if ($spacePoint->hash === $hash) { | ||
| return $spacePoint; | ||
| } | ||
| } | ||
| return null; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just wondering: shouldn't this fail instead?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed: We probably don't even need to expose this
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use this in the changes made to Flowpack.Media.Ui currently, but it could be replaced by using the new map() implementation and filtering on the consumer-side. So I concur, I'll remove it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's definitely not an important point. But if we remove it, we don't have to settle on the failure behavior :) |
||
| } | ||
|
|
||
| public function getIterator(): Traversable | ||
| { | ||
| yield from $this->spacePoints; | ||
| } | ||
|
|
||
| public function getHashIterator(): Traversable | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: It's called 'dimensionHashes' => iterator_to_array($dimensionSpacePoints->getHashIterator()),by 'dimensionHashes' => $dimensionSpacePoints->getHashes(),or: don't expose them and go for final readonly class MetaDataDimensionSpacePoints implements IteratorAggregate {
// ...
/**
* @template T
* @param Closure(MetaDataDimensionSpacePoint): T
* @return T[]
*/
public function map(Closure $callback): array {
return array_map($callback, $this->spacePoints);
}and 'dimensionHashes' => $dimensionSpacePoints->map($spacePoint->hash) |
||
| { | ||
| foreach ($this->spacePoints as $spacePoint) { | ||
| yield $spacePoint->hash; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <?php | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Neos\MetaData\Helper; | ||
|
|
||
| use Neos\Eel\ProtectedContextAwareInterface; | ||
| use Neos\Media\Domain\Model\Asset; | ||
| use Neos\MetaData\Domain\Dto\MetaDataAssetReference; | ||
| use Neos\MetaData\Domain\Dto\MetaDataDimensionSpacePoint; | ||
| use Neos\MetaData\Domain\Dto\MetaDataPropertyName; | ||
| use Neos\MetaData\MetaDataManager; | ||
|
|
||
| class AssetMetaDataHelper implements ProtectedContextAwareInterface | ||
| { | ||
|
|
||
| public function __construct( | ||
| protected MetaDataManager $metaDataManager, | ||
| ) | ||
| { | ||
| } | ||
|
|
||
| public function getMetaData(Asset $asset, array $coordinates = []): array | ||
| { | ||
| $propertyValues = $this->metaDataManager->getMetaDataPropertyValues( | ||
| MetaDataAssetReference::fromAsset($asset), | ||
| MetaDataDimensionSpacePoint::fromCoordinates($coordinates), | ||
| ); | ||
| $result = []; | ||
| foreach ($propertyValues as $propertyName => $propertyValue) { | ||
| /** @var $propertyName MetaDataPropertyName */ | ||
| $result[$propertyName->value] = $propertyValue; | ||
| } | ||
| return $result; | ||
| } | ||
|
|
||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| public function allowsCallOfMethod($methodName) | ||
| { | ||
| return in_array($methodName, ['getMetaData']); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code was more or less copied from the original AbstractDimensionSpacePoint.php.
AFAIK, the DSP is a concrete point in the dimension – now this is rather describing the whole dimension space
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made this change to the structure to achieve the same hashing result as in the CR in v8. The dsp hashes are generated by hashing over exactly this structure (dimensions => [value]). I honestly don't know why it is done that way but I wanted to align the hashes.
But yeah, technically this could describe the entire dimension space. I don't have a strong opinion on this one, it's probably clearer to the intent if we do it correctly. The only thing this would mean is that we don't have a direct relation between the dsp-hashes of the old cr and ours.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I was looking at the Neos 9 code where the hash for empty is
d751713988987e9331980363e24189ce(which is equivalent tomd5(json_encode([]))) and the hash for en_UK is046d33049fe2ab3d1c3c54ca5340186fwhich is the equivalent tomd5(json_encode(['language' => 'en_US'])).But you are correct, that this is stored differently in Neos 8 – I wasn't aware of that.
I would suggest to keep the DTOs clean though and somehow "translate" it in the concrete v8 adapter if possible