classes/Model/Repository/Repository.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
/**
 * Fixin Framework
 *
 * Copyright (c) Attila Jenei
 *
 * http://www.fixinphp.com
 */

namespace Fixin\Model\Repository;

use DateTimeImmutable;
use Fixin\Model\Entity\Cache\CacheInterface;
use Fixin\Model\Entity\EntityIdInterface;
use Fixin\Model\Entity\EntityInterface;
use Fixin\Model\Entity\EntitySetInterface;
use Fixin\Model\Request\ExpressionInterface;
use Fixin\Model\Request\RequestInterface;
use Fixin\Model\Storage\StorageInterface;
use Fixin\Model\Storage\StorageResultInterface;
use Fixin\Resource\Resource;
use Fixin\Support\Arrays;
use Fixin\Support\Types;

class Repository extends Resource implements RepositoryInterface
{
    protected const
        ENTITY_ID_PROTOTYPE = '*\Model\Entity\EntityId',
        ENTITY_REFRESH_FAILURE_EXCEPTION = 'Entity refresh error',
        ENTITY_SET_PROTOTYPE = '*\Model\Entity\EntitySet',
        EXPRESSION_PROTOTYPE = '*\Model\Request\Expression',
        INVALID_ID_EXCEPTION = "Invalid ID",
        INVALID_NAME_EXCEPTION = "Invalid name '%s'",
        INVALID_REQUEST_EXCEPTION = "Invalid request, repository mismatch '%s' '%s'",
        NAME_PATTERN = '/^[a-zA-Z_][a-zA-Z0-9_]*$/',
        NOT_STORED_ENTITY_EXCEPTION = 'Not stored entity',
        REQUEST_PROTOTYPE = '*\Model\Request\Request',
        THIS_SETS = [
            self::AUTO_INCREMENT_COLUMN => [Types::STRING, Types::NULL],
            self::ENTITY_CACHE => [self::LAZY_LOADING => CacheInterface::class],
            self::ENTITY_PROTOTYPE => [self::LAZY_LOADING => EntityInterface::class],
            self::NAME => self::USING_SETTER,
            self::PRIMARY_KEY => Types::ARRAY,
            self::STORAGE => [self::LAZY_LOADING => StorageInterface::class]
        ];

    /**
     * @var string|null
     */
    protected $autoIncrementColumn;

    /**
     * @var CacheInterface|false
     */
    protected $entityCache;

    /**
     * @var EntityInterface|false
     */
    protected $entityPrototype;

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string[]
     */
    protected $primaryKey = ['id'];

    /**
     * @var StorageInterface|false
     */
    protected $storage;

    public function create(): EntityInterface
    {
        return clone $this->getEntityPrototype();
    }

    public function createExpression(string $expression, array $parameters = []): ExpressionInterface
    {
        return $this->resourceManager->clone(static::EXPRESSION_PROTOTYPE, ExpressionInterface::class, [
            ExpressionInterface::EXPRESSION => $expression,
            ExpressionInterface::PARAMETERS => $parameters
        ]);
    }

    public function createId(...$entityId): EntityIdInterface
    {
        $columnCount = count($this->primaryKey);

        // Array
        if (is_array($entityId[0])) {
            $entityId = array_intersect_key($entityId[0], array_flip($this->primaryKey));

            if (count($entityId) === $columnCount) {
                return $this->createIdWithArray($entityId);
            }

            throw new Exception\InvalidArgumentException(static::INVALID_ID_EXCEPTION);
        }

        // List
        if (count($entityId) === $columnCount) {
            return $this->createIdWithArray(array_combine($this->primaryKey, $entityId));
        }

        throw new Exception\InvalidArgumentException(static::INVALID_ID_EXCEPTION);
    }

    private function createIdWithArray(array $entityId): EntityIdInterface
    {
        return $this->resourceManager->clone(static::ENTITY_ID_PROTOTYPE, EntityIdInterface::class, [
            EntityIdInterface::ENTITY_ID => $entityId,
            EntityIdInterface::REPOSITORY => $this
        ]);
    }

    public function createRequest(): RequestInterface
    {
        return $this->resourceManager->clone(static::REQUEST_PROTOTYPE, RequestInterface::class, [
            RequestInterface::REPOSITORY => $this
        ]);
    }

    public function delete(RequestInterface $request): int
    {
        $this->validateRequest($request);

        if ($result = $this->getStorage()->delete($request)) {
            $this->getEntityCache()->invalidate();
        }

        return $result;
    }

    public function deleteByIds(array $ids): int
    {
        $request = $this->createRequest();
        $request->getWhere()->ids($ids);

        return $this->delete($request);
    }

    public function getAutoIncrementColumn(): ?string
    {
        return $this->autoIncrementColumn;
    }

    public function getById(EntityIdInterface $id): ?EntityInterface
    {
        $entities = $this->getEntityCache()->getByIds([$id]);

        return reset($entities);
    }

    public function getByIds(array $ids): EntitySetInterface
    {
        return $this->resourceManager->clone(static::ENTITY_SET_PROTOTYPE, EntitySetInterface::class, [
            EntitySetInterface::REPOSITORY => $this,
            EntitySetInterface::ENTITY_CACHE => $this->getEntityCache(),
            EntitySetInterface::ITEMS => $this->getEntityCache()->getByIds($ids)
        ]);
    }

    protected function getEntityCache(): CacheInterface
    {
        return $this->entityCache ?: $this->loadLazyProperty(static::ENTITY_CACHE, [
            CacheInterface::REPOSITORY => $this,
            CacheInterface::ENTITY_PROTOTYPE => $this->getEntityPrototype()
        ]);
    }

    protected function getEntityPrototype(): EntityInterface
    {
        return $this->entityPrototype ?: $this->loadLazyProperty(static::ENTITY_PROTOTYPE, [
            EntityInterface::REPOSITORY => $this
        ]);
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPrimaryKey(): array
    {
        return $this->primaryKey;
    }

    protected function getStorage(): StorageInterface
    {
        return $this->storage ?: $this->loadLazyProperty(static::STORAGE);
    }

    public function insert(array $set): EntityIdInterface
    {
        if ($this->getStorage()->insert($this, $set)) {
            $rowId = Arrays::intersectByKeyList($set, $this->primaryKey);

            if (isset($this->autoIncrementColumn)) {
                $rowId[$this->autoIncrementColumn] = $this->storage->getLastInsertValue();
            }

            return $this->createIdWithArray($rowId);
        }

        return null;
    }

    public function insertInto(RepositoryInterface $repository, RequestInterface $request): int
    {
        $this->validateRequest($request);

        return $this->getStorage()->insertInto($repository, $request);
    }

    public function insertMultiple(array $rows): int
    {
        return $this->getStorage()->insertMultiple($this, $rows);
    }

    /**
     * @throws Exception\EntityRefreshFaultException
     * @throws Exception\NotStoredEntityException
     *
     * @return $this
     */
    public function refresh(EntityInterface $entity): RepositoryInterface
    {
        if ($entity->isStored()) {
            $request = $this->createRequest();
            $request->getWhere()->id($entity->getEntityId());
            $data = $request->fetchRawData()->current();

            if ($data !== false) {
                $entity->exchangeArray($data);
                $this->getEntityCache()->update($entity);

                return $this;
            }

            throw new Exception\EntityRefreshFaultException(static::ENTITY_REFRESH_FAILURE_EXCEPTION);
        }

        throw new Exception\NotStoredEntityException(static::NOT_STORED_ENTITY_EXCEPTION);
    }

    public function save(EntityInterface $entity): EntityIdInterface
    {
        $set = $entity->collectSaveData();

        if ($oldId = $entity->getEntityId()) {
            $request = $this->createRequest();
            $request->getWhere()->id($oldId);
            $this->getStorage()->update($set, $request);

            $id = array_replace($oldId->getArrayCopy(), Arrays::intersectByKeyList($set, $this->primaryKey));
            if ($id === $oldId->getArrayCopy()) {
                return $oldId;
            }

            $this->getEntityCache()->remove($entity);

            return $this->createIdWithArray($id);
        }

        return $this->insert($set);
    }

    public function select(RequestInterface $request): EntitySetInterface
    {
        $fetchRequest = clone $request;
        $fetchRequest->setColumns($fetchRequest->isIdFetchEnabled() ? $this->primaryKey : []);

        return $this->resourceManager->clone(static::ENTITY_SET_PROTOTYPE, EntitySetInterface::class, [
            EntitySetInterface::REPOSITORY => $this,
            EntitySetInterface::ENTITY_CACHE => $this->getEntityCache(),
            EntitySetInterface::STORAGE_RESULT => $this->selectRawData($fetchRequest),
            EntitySetInterface::ID_FETCH_MODE => $fetchRequest->isIdFetchEnabled()
        ]);
    }

    public function selectAll(): EntitySetInterface
    {
        return $this->createRequest()->fetch();
    }

    public function selectColumn(RequestInterface $request): StorageResultInterface
    {
        return $this->getStorage()->selectColumn($request);
    }

    public function selectExistsValue(RequestInterface $request): bool
    {
        $this->validateRequest($request);

        return $this->getStorage()->selectExistsValue($request);
    }

    public function selectRawData(RequestInterface $request): StorageResultInterface
    {
        return $this->getStorage()->select($request);
    }

    /**
     * @throws Exception\InvalidArgumentException
     */
    protected function setName(string $name): void
    {
        if (preg_match(static::NAME_PATTERN, $name)) {
            $this->name = $name;

            return;
        }

        throw new Exception\InvalidArgumentException(sprintf(static::INVALID_NAME_EXCEPTION, $name));
    }

    public function toDateTime($value): ?DateTimeImmutable
    {
        return $this->getStorage()->toDateTime($value);
    }

    public function update(array $set, RequestInterface $request): int
    {
        $this->validateRequest($request);

        if ($result = $this->getStorage()->update($set, $request)) {
            $this->getEntityCache()->invalidate();
        }

        return $result;
    }

    /**
     * @throws Exception\InvalidRequestException
     */
    protected function validateRequest(RequestInterface $request): void
    {
        if ($request->getRepository() === $this) {
            return;
        }

        throw new Exception\InvalidRequestException(sprintf(static::INVALID_REQUEST_EXCEPTION, $this->getName(), $request->getRepository()->getName()));
    }
}