Skip to content
48 changes: 48 additions & 0 deletions Classes/Command/AssetMetaDataMigrationCommandController.php
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(),
);
}
}
}
}
14 changes: 14 additions & 0 deletions Classes/Configuration/MetaDataConfigurationProvider.php
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;
}
59 changes: 59 additions & 0 deletions Classes/Configuration/MetaDataConfigurationProviderYamlAdapter.php
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
Expand Up @@ -16,5 +16,5 @@ public function getDefaultDimensionSpacePoint(): MetaDataDimensionSpacePoint;

public function getDimensionSpacePointChain(MetaDataDimensionSpacePoint $dimensionSpacePoint): MetaDataDimensionSpacePoints;

// public function RENAMEisDimensionSpacePointValid(MetaDataDimensionSpacePoint $dimensionSpacePoint): bool;
public function isDimensionSpacePointValid(MetaDataDimensionSpacePoint $dimensionSpacePoint): bool;
}
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);
}
}
8 changes: 8 additions & 0 deletions Classes/Domain/Dto/MetaDataAssetReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ public function __construct(
public string $assetId,
) {
}

public static function fromAsset(Asset $asset): self
{
return new self(
$asset->getAssetSourceIdentifier(),
$asset->getIdentifier(),
);
}
}
10 changes: 4 additions & 6 deletions Classes/Domain/Dto/MetaDataDimensionSpacePoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@

/**
* A point in the dimension space with coordinates DimensionName => DimensionValue.
* E.g.: ["language" => "es", "country" => "ar"]
* E.g.: ["language" => ["es"], "country" => ["ar"]]
Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Author

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.

Copy link
Copy Markdown
Member

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 to md5(json_encode([]))) and the hash for en_UK is 046d33049fe2ab3d1c3c54ca5340186f which is the equivalent to md5(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

*/
final readonly class MetaDataDimensionSpacePoint implements Stringable {

/**
* @param array<string,string> $coordinates
* @param array<string,string[]> $coordinates
* @param string $hash
*/
private function __construct(
Expand All @@ -24,7 +24,7 @@ private function __construct(
}

/**
* @param array<string,string> $coordinates
* @param array<string,string[]> $coordinates
*/
private static function hashCoordinates(array $coordinates): string
{
Expand All @@ -38,7 +38,7 @@ private static function hashCoordinates(array $coordinates): string
}

/**
* @param array<string,string> $coordinates
* @param array<string,string[]> $coordinates
*/
public static function fromCoordinates(array $coordinates): self
{
Expand All @@ -50,8 +50,6 @@ public static function fromCoordinates(array $coordinates): self

public function equals(self $other): bool
{
\Neos\Flow\var_dump($this, 'this');
\Neos\Flow\var_dump($other, 'pther');
return $this->hash === $other->hash;
}

Expand Down
17 changes: 17 additions & 0 deletions Classes/Domain/Dto/MetaDataDimensionSpacePoints.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wondering: shouldn't this fail instead?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed: We probably don't even need to expose this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: It's called getIterator above only because that's the interface, but it is not meant to be invoked manually.
If we expose all hashes I would suggest to replace

   '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;
}
}
}
43 changes: 43 additions & 0 deletions Classes/Helper/AssetMetaDataHelper.php
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']);
}
}
Loading