Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Neos\MetaData\Domain\Dto\MetaDataPropertyDefinition;
use Neos\MetaData\Domain\Dto\MetaDataPropertyDefinitions;
use Neos\MetaData\Domain\Dto\MetaDataPropertyName;
use Neos\MetaData\Domain\Dto\MetaDataPropertyOptions;
use Neos\MetaData\Domain\Dto\MetaDataPropertyType;
use Neos\MetaData\Domain\Dto\MetaDataPropertyUiDefinition;

Expand All @@ -33,6 +34,9 @@ public function getPropertyConfiguration(): MetaDataPropertyDefinitions
match ($propertyDefinition['type'] ?? null) {
'integer' => MetaDataPropertyType::integer,
'boolean' => MetaDataPropertyType::boolean,
'float' => MetaDataPropertyType::float,
'array' => MetaDataPropertyType::array,
'dateTime' => MetaDataPropertyType::dateTime,
default => MetaDataPropertyType::string,
},
$propertyDefinition['globalScope'] ?? false,
Expand All @@ -43,6 +47,9 @@ public function getPropertyConfiguration(): MetaDataPropertyDefinitions
options: $propertyDefinition['ui']['inspector']['editorOptions'] ?? [],
)
) : null,
array_key_exists('options', $propertyDefinition) && is_array($propertyDefinition['options'])
? $propertyDefinition['options']
: null,
);
}
return MetaDataPropertyDefinitions::create(...$propertyDefinitions);
Expand Down
1 change: 1 addition & 0 deletions Classes/Domain/Dto/MetaDataPropertyDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __construct(
public MetaDataPropertyType $type,
public bool $globalScope,
public ?MetaDataPropertyUiDefinition $ui = null,
public ?array $options = 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.

What are these? Can we add some @paramannotation with type + description

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.

is this related and used by the new code at all?

) {
}
}
108 changes: 100 additions & 8 deletions Classes/Domain/Dto/MetaDataPropertyType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@

namespace Neos\MetaData\Domain\Dto;

use DateTimeImmutable;
use DateTimeInterface;
use InvalidArgumentException;
use JsonException;
use Throwable;

/**
* Type of a custom asset metadata property.
*
* Values are stored as strings, so this is also what turns a value into its stored representation and
* back: {@see self::coerceForStorage()} on the way in, {@see self::fromStoredValue()} on the way out.
* The concrete PHP type of a value is case-dependent - see the private `to*()` helpers below for what
* each case accepts and returns. Callers elsewhere in the domain therefore type a logical value as
* `mixed` rather than repeating a union of every case's type.
*
* The two directions are deliberately not equally strict. Writing rejects what it cannot interpret,
* because a caller passing "abc" for an integer property has made a mistake that should not be
Expand All @@ -22,57 +29,86 @@ enum MetaDataPropertyType
case string;
case integer;
case boolean;
case float;
case array;

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.

do we really need to support arrays? if so I think that we should limit it to array<string|int|bool|float>

case dateTime;

/**
* The given value in the representation it is stored as.
*
* Unambiguous conversions are applied, so that callers which only ever have strings – the command
* line, form input, Fusion – do not have to cast: "42" is a valid integer, "true", "on" and "yes"
* are a valid boolean, as are their negative counterparts. Anything else is rejected.
* are a valid boolean, as are their negative counterparts, a JSON encoded string is a valid array
* and an ISO 8601 string is a valid date and time. Anything else is rejected.
*
* @throws InvalidArgumentException if the value cannot be interpreted as this type
*/
public function coerceForStorage(string|int|bool $value): string
public function coerceForStorage(mixed $value): string

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.

Isn't string|int|bool|float|array|DateTimeInterface still better here?

{
$coerced = $this->tryCoerce($value);
if ($coerced === null) {
throw new InvalidArgumentException(sprintf('Value %s cannot be interpreted as %s', json_encode($value), $this->name), 1785715201);
}
return is_bool($coerced) ? ($coerced ? '1' : '0') : (string)$coerced;
return match (true) {
is_bool($coerced) => $coerced ? '1' : '0',
is_array($coerced) => json_encode($coerced, JSON_THROW_ON_ERROR),
$coerced instanceof DateTimeInterface => $coerced->format(DATE_ATOM),

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.

Do we agree that DATE_ATOM is the right way to represent datetime always?

default => (string)$coerced,
};
}

/**
* The given stored value as this type, or NULL if it cannot be interpreted as one
*/
public function fromStoredValue(string|int|bool $value): string|int|bool|null
public function fromStoredValue(string $value): mixed
{
return $this->tryCoerce($value);
}

// -----------------------

private function tryCoerce(string|int|bool $value): string|int|bool|null
private function tryCoerce(mixed $value): mixed
{
if ($value === null) {
return null;
}
return match ($this) {
self::string => is_bool($value) ? ($value ? '1' : '0') : (string)$value,
self::string => self::toString($value),
self::integer => self::toInteger($value),
self::boolean => self::toBoolean($value),
self::float => self::toFloat($value),
self::array => self::toArray($value),
self::dateTime => self::toDateTime($value),
};
}

private static function toInteger(string|int|bool $value): ?int
private static function toString(mixed $value): ?string
{
if (is_bool($value)) {
return $value ? '1' : '0';
}
if (is_string($value) || is_int($value) || is_float($value)) {
return (string)$value;
}
return null;
}

private static function toInteger(mixed $value): ?int
{
if (is_int($value)) {
return $value;
}
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (!is_string($value)) {
return null;
}
$trimmed = trim($value);
return preg_match('/^-?\d+$/', $trimmed) === 1 ? (int)$trimmed : null;
}

private static function toBoolean(string|int|bool $value): ?bool
private static function toBoolean(mixed $value): ?bool
{
if (is_bool($value)) {
return $value;
Expand All @@ -84,10 +120,66 @@ private static function toBoolean(string|int|bool $value): ?bool
default => null,
};
}
if (!is_string($value)) {
return null;
}
return match (strtolower(trim($value))) {
'1', 'true', 'on', 'yes' => true,
'0', 'false', 'off', 'no' => false,
default => null,
};
}

private static function toFloat(mixed $value): ?float
{
if (is_float($value) && !is_nan($value) && !is_infinite($value)) {
return $value;
}
if (is_int($value)) {
return (float)$value;
}
if (!is_string($value)) {
return null;
}
$trimmed = trim($value);
return preg_match('/^-?\d+(\.\d+)?$/', $trimmed) === 1 ? (float)$trimmed : null;
}

private static function toArray(mixed $value): ?array
{
if (is_array($value)) {
return $value;
}
if (!is_string($value)) {
return null;
}
try {
$decoded = json_decode($value, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException) {
return null;
}
return is_array($decoded) ? $decoded : null;
}

private static function toDateTime(mixed $value): ?DateTimeImmutable
{
if ($value instanceof DateTimeImmutable) {
return $value;
}
if ($value instanceof DateTimeInterface) {
return DateTimeImmutable::createFromInterface($value);
}
if (!is_string($value)) {
return null;
}
$parsed = DateTimeImmutable::createFromFormat(DATE_ATOM, $value);
if ($parsed !== false) {
return $parsed;
}
try {
return new DateTimeImmutable($value);
} catch (Throwable) {
return null;
}
}
}
18 changes: 9 additions & 9 deletions Classes/Domain/Dto/MetaDataPropertyValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@
final readonly class MetaDataPropertyValue
{
/**
* @param string|int|bool|null $value the effective value, i.e. the own value falling back to the inherited one
* @param string|int|bool|null $ownValue the value stored for the dimension space point that was asked for
* @param string|int|bool|null $inheritedValue the value stored for the closest fallback dimension space point
* @param mixed $value the effective value, i.e. the own value falling back to the inherited one - see {@see MetaDataPropertyType} for the concrete type
* @param mixed $ownValue the value stored for the dimension space point that was asked for
* @param mixed $inheritedValue the value stored for the closest fallback dimension space point
* @param MetaDataDimensionSpacePoint|null $inheritedFrom the dimension space point the inherited value stems from
*/
private function __construct(
public string|int|bool|null $value,
public string|int|bool|null $ownValue,
public string|int|bool|null $inheritedValue,
public mixed $value,
public mixed $ownValue,
public mixed $inheritedValue,
Comment on lines +31 to +33

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.

mixed is the broadest type that we can use. I would prefer union so that consumers can match over all cases, e.g. if they need to serialize these

public ?MetaDataDimensionSpacePoint $inheritedFrom,
) {
}

public static function create(
string|int|bool|null $ownValue,
string|int|bool|null $inheritedValue = null,
mixed $ownValue,
mixed $inheritedValue = null,
?MetaDataDimensionSpacePoint $inheritedFrom = null,
): self {
return new self(
Expand All @@ -63,7 +63,7 @@ public function hasOwnValue(): bool
}

/** Fusion getter access */
public function getOwnValue(): string|int|bool|null
public function getOwnValue(): mixed
{
return $this->ownValue;
}
Expand Down
2 changes: 1 addition & 1 deletion Classes/Domain/Dto/MetaDataPropertyValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function get(MetaDataPropertyName $propertyName): MetaDataPropertyValue
/**
* The effective values by property name, e.g. for rendering
*
* @return array<string, string|int|bool|null>
* @return array<string, mixed>
*/
public function toArray(): array
{
Expand Down
7 changes: 4 additions & 3 deletions Classes/Helper/AssetMetaDataHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Neos\Media\Domain\Model\Asset;
use Neos\MetaData\Domain\Dto\MetaDataAssetReference;
use Neos\MetaData\Domain\Dto\MetaDataDimensionSpacePoint;
use Neos\MetaData\Domain\Dto\MetaDataPropertyType;
use Neos\MetaData\MetaDataManager;

class AssetMetaDataHelper implements ProtectedContextAwareInterface
Expand All @@ -22,7 +23,7 @@ public function __construct(
* The effective metadata of the given asset by property name, with dimension fallbacks applied
*
* @param array<string,string> $coordinates dimension coordinates, e.g. ['language' => 'de']. Empty = the default dimension
* @return array<string, string|int|bool|null>
* @return array<string, mixed>
*/
public function getMetaData(Asset $asset, array $coordinates = []): array
{
Expand All @@ -36,9 +37,9 @@ public function getMetaData(Asset $asset, array $coordinates = []): array
* The effective metadata property value of the given asset and property name, with dimension fallbacks applied
*
* @param array<string,string> $coordinates dimension coordinates, e.g. ['language' => 'de']. Empty = the default dimension
* @return string|int|bool|null Value of the metadata property or NULL if it was not set (or explicitly reset)
* @return mixed Value of the metadata property, whose concrete type is defined by {@see MetaDataPropertyType}, or NULL if it was not set (or explicitly reset)
*/
public function getMetaDataProperty(Asset $asset, string $propertyName, array $coordinates = []): string|int|bool|null
public function getMetaDataProperty(Asset $asset, string $propertyName, array $coordinates = []): mixed
{
return $this->metaDataManager->getMetaDataPropertyValue(
MetaDataAssetReference::create($asset->assetSourceIdentifier, $asset->getIdentifier()),
Expand Down
2 changes: 1 addition & 1 deletion Classes/MetaDataManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function getDimensionSpacePointConfiguration(): MetaDataDimensionSpacePoi
public function setMetaDataPropertyValue(
MetaDataAssetReference $assetReference,
MetaDataPropertyName|string $propertyName,
string|int|bool $value,
mixed $value,
?MetaDataDimensionSpacePoint $dimensionSpacePoint = null,
): void {
$propertyDefinition = $this->propertyDefinition($propertyName);
Expand Down
4 changes: 2 additions & 2 deletions Classes/Storage/MetaDataStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
interface MetaDataStorage
{

public function setMetaDataPropertyValue(MetaDataAssetReference $assetReference, MetaDataPropertyName $propertyName, string|int|bool $propertyValue, MetaDataDimensionSpacePoint|MetaDataGlobalScope $scope): void;
public function setMetaDataPropertyValue(MetaDataAssetReference $assetReference, MetaDataPropertyName $propertyName, string $propertyValue, MetaDataDimensionSpacePoint|MetaDataGlobalScope $scope): void;

public function unsetMetaDataPropertyValue(MetaDataAssetReference $assetReference, MetaDataPropertyName $propertyName, MetaDataDimensionSpacePoint|MetaDataGlobalScope $scope): void;

Expand All @@ -35,7 +35,7 @@ public function unsetMetaDataPropertyValue(MetaDataAssetReference $assetReferenc
* be compared with {@see MetaDataDimensionSpacePoint::$hash}. Scopes without a stored value are
* absent from the result.
*
* @return array<string, string|int|bool>
* @return array<string, string>
*/
public function getMetaDataPropertyValues(MetaDataAssetReference $assetReference, MetaDataPropertyName $propertyName, MetaDataDimensionSpacePoints|MetaDataGlobalScope $scope): array;

Expand Down
2 changes: 1 addition & 1 deletion Classes/Storage/MetaDataStorageProviderDbalAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function __construct(
) {
}

public function setMetaDataPropertyValue(MetaDataAssetReference $assetReference, MetaDataPropertyName $propertyName, string|int|bool $propertyValue, MetaDataDimensionSpacePoint|MetaDataGlobalScope $scope): void
public function setMetaDataPropertyValue(MetaDataAssetReference $assetReference, MetaDataPropertyName $propertyName, string $propertyValue, MetaDataDimensionSpacePoint|MetaDataGlobalScope $scope): void
{
$statement = sprintf(<<<MYSQL
INSERT INTO %s
Expand Down
Loading