RFC: PIM x Property Graph: EAV, Reference Types, Constraints, Neos 9.3 and all the good stuff

OPGM Considerations

(as I will be using it for the project and it tends to bring a lot of clarity into things by disallowing any kind of array soup)

Attribute references

We cannot declare complete reference types as declared above as OPGM objects as they are resolved via ContentSubgraphInterface::getReferences() and ::getBackReferences, so they only ever contain the reference properties and either source or target node. Which is totally fine but needs a few decisions:

In the OPGM world, reference relations change meaning depending on perspective. Let’s consider the above example:

#[NodeTypeDeclaration]
public readonly class FloatValueDimension implements Attribute
{
    public function __construct(
        public string $name,
        public string $code,
        public string $unit,
    ) {
    }
}

public readonly class FloatAttributeValue implements AttributeValue, ReferenceRelation
{
    public function __construct(
        public ?float $value,
        public ?float $minValue,
        public ?float $maxValue,
        public FloatValueDimension $attribute,
    ) {
    }
}

/** @implements \IteratorAggregate<AttributeValue> */
public readonly class AttributeValues implements \IteratorAggregate
{
    [...]
}

#[NodeTypeDeclaration]
public readonly class Product
{
    public function __construct(
        public AttributeValues $attributeValues,
    ) {
    }

    public function getLength(): ?float
    {
        foreach ($this->attributeValues as $attributeValue) {
            if (
                $attributeValue instanceof FloatAttributeValue
                && $attributeValue->attribute->code === UNCEFACT::CODE_LENGTH
            ) {
                return $attributeValue->value;
            } 
        }

        return null;
    }
}

We can now see that FloatAttributeValue is directly related to FloatValueDimension and can derive that part of the reference type declaration from that class. What’s still missing is the back reference part; We know that products in general reference attribute values, but that’s not quite enought yet to make a general assumption.

If we consider the length attribute of type FloatValueDimension, the back reference has a different shape because we now want to know the related products instead of the attributes:

#[BackReferenceRelation(referenceRelation: FloatAttributeValue::class)]
public readonly class FloatProductValue implements ProductValue
{
    public function __construct(
        public ?float $value,
        public ?float $minValue,
        public ?float $maxValue,
        public Product $product,
    ) {
    }
}

/** @implements \IteratorAgggregate<FloatProductValue> */
public readonly class FloatProductValues implements \IteratorAggregate
{
    [...]
}

#[NodeTypeDeclaration]
public readonly class FloatValueDimension implements Attribute
{
    public function __construct(
        public string $name,
        public string $code,
        public string $unit,
        public FloatProductValues $productValues,
    ) {
    }
}

This way, we get both directions, have a type-safe connection between BackReference and Reference via the attribute and can constitute the complete ReferenceRelation declaration.

General references

If we want to implement our product comparison model, we can do so without declaring any kind of reference type because we don’t need any additional information (with Products and Attributes being simple typed collections):

#[NodeTypeDeclaration]
public readonly class ProductComparison implements Content
{
    public function __construct(
        #[ReferenceRelationDeclaration]
        public Products $products,
        #[ReferenceRelationDeclaration]
        public Attributes $attributes,
    ) {
    }
}

The relations between ProductComparison and Product or Attribute don’t need a type at all, which hints to the type of a reference being nullable if they don’t carry any properties. They are never resolved themselves, only the nodes are relevant.

Conclusion

This could be used to simplify our model: Neos.ContentRepository:Featureless does not exist and does not need to be constrained. If explicit reference types are available for source and target, the generic, empty reference is disallowed. We might get rid of the use cases as well by defining explicit back/reference types instead (traits 4tw).