daikon-cqrs/entity

View on GitHub
src/Entity.php

Summary

Maintainability
A
0 mins
Test Coverage
A
90%
<?php declare(strict_types=1);
/**
 * This file is part of the daikon-cqrs/entity project.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Daikon\Entity;

use Daikon\Entity\Path\ValuePathParser;
use Daikon\Entity\Path\ValuePathPart;
use Daikon\Interop\Assertion;
use Daikon\ValueObject\ValueObjectInterface;
use Daikon\ValueObject\ValueObjectMap;

abstract class Entity implements EntityInterface
{
    private ValueObjectMap $valueObjectMap;

    private ValuePathParser $pathParser;

    final protected function __construct(array $state = [])
    {
        $this->pathParser = ValuePathParser::create();

        $objects = [];
        foreach ($this->getAttributeMap() as $name => $attribute) {
            if (array_key_exists($name, $state)) {
                $objects[$name] = $attribute->makeValue($state[$name]);
            }
        }

        $this->valueObjectMap = new ValueObjectMap($objects);
    }

    /**
     * @param array $state
     * @return static
     */
    public static function fromNative($state): self
    {
        return new static($state);
    }

    public function toNative(): array
    {
        $entityState = $this->valueObjectMap->toNative();
        $entityState[EntityInterface::TYPE_KEY] = static::class;
        return $entityState;
    }

    /** @param static $entity */
    public function isSameAs($entity): bool
    {
        Assertion::isInstanceOf($entity, static::class);
        return $this->getIdentity()->equals($entity->getIdentity());
    }

    public function has(string $name): bool
    {
        $has = $this->getAttributeMap()->has($name);
        Assertion::true($has, sprintf("Attribute '%s' is not known to the entity '%s'.", $name, static::class));
        return $this->valueObjectMap->has($name);
    }

    public function get(string $name, $default = null): ?ValueObjectInterface
    {
        if (mb_strpos($name, '.')) {
            return $this->evaluatePath($name);
        }

        $attribute = $this->getAttributeMap()->get($name, null);
        Assertion::notNull($attribute, sprintf("Attribute '%s' is not known to entity '%s'.", $name, static::class));
        /** @psalm-suppress PossiblyNullArgument */
        $attributeType = get_class($attribute);
        Assertion::nullOrIsInstanceOf($default, $attributeType, sprintf(
            "Default type for '$name' must be null or $attributeType, but got '%s'.",
            is_object($default) ? get_class($default) : @gettype($default)
        ));

        return $this->valueObjectMap->get($name, $default);
    }

    /** @return static */
    public function withValue(string $name, $value): self
    {
        $copy = clone $this;
        $copy->valueObjectMap = $copy->valueObjectMap->with($name, $this->makeValue($name, $value));
        return $copy;
    }

    /** @return static */
    public function withValues(iterable $values): self
    {
        $copy = clone $this;
        foreach ($values as $name => $value) {
            $copy->valueObjectMap = $copy->valueObjectMap->with($name, $this->makeValue($name, $value));
        }
        return $copy;
    }

    /** @param static $comparator */
    public function equals($comparator): bool
    {
        Assertion::isInstanceOf($comparator, static::class);
        return (new EntityDiff)($this, $comparator)->isEmpty();
    }

    public function __toString(): string
    {
        return (string)$this->getIdentity();
    }

    /** @param mixed $value */
    private function makeValue(string $name, $value): ValueObjectInterface
    {
        $attribute = $this->getAttributeMap()->get($name, null);
        Assertion::isInstanceOf(
            $attribute,
            AttributeInterface::class,
            sprintf("Attribute '%s' is unknown to entity '%s'.", $name, static::class)
        );
        /** @var AttributeInterface $attribute */
        return $attribute->makeValue($value);
    }

    private function evaluatePath(string $valuePath): ?ValueObjectInterface
    {
        $entity = $this;
        /** @var ValuePathPart $pathPart */
        foreach ($this->pathParser->parse($valuePath) as $pathPart) {
            $name = $pathPart->getAttributeName();
            $value = $entity ? $entity->get($name) : null;
            if ($value && $pathPart->hasPosition()) {
                Assertion::isInstanceOf(
                    $value,
                    EntityList::class,
                    sprintf("Trying to traverse a non-entity list '%s' on entity '%s'.", $name, static::class)
                );
                /** @var EntityList $value */
                $entity = $value->get($pathPart->getPosition());
                $value = $entity;
            }
        }
        return $value ?? null;
    }

    public function __get(string $attribute)
    {
        return $this->get($attribute);
    }

    public function __clone()
    {
        $this->pathParser = clone $this->pathParser;
        $this->valueObjectMap = clone $this->valueObjectMap;
    }
}