edmondscommerce/doctrine-static-meta

View on GitHub
src/Entity/Repositories/AbstractEntityRepository.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

declare(strict_types=1);

namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Repositories;

use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\LazyCriteriaCollection;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
use EdmondsCommerce\DoctrineStaticMeta\Entity\Factory\EntityFactoryInterface;
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
use Ramsey\Uuid\UuidInterface;
use RuntimeException;

use function str_replace;

/**
 * Class AbstractEntityRepository
 *
 * This provides a base class that handles instantiating the correctly configured EntityRepository and provides an
 * extensible baseline for further customisation
 *
 * We have extracted an interface from the standard Doctrine EntityRepository and implemented that
 * However, so we can add type safety, we can't "actually" implement it
 *
 * We have also deliberately left out the magic calls. Please make real methods in your concrete repository class
 *
 * Note, there are quite a few PHPMD warnings, however it needs to respect the legacy interface so they are being
 * suppressed
 *
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity\Repositories
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 * @SuppressWarnings(PHPMD.ExcessivePublicCount)
 * @SuppressWarnings(PHPMD.NumberOfChildren)
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
abstract class AbstractEntityRepository implements EntityRepositoryInterface
{
    /**
     * @var array
     */
    protected static $aliasCache;
    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;
    /**
     * @var EntityRepository
     */
    protected $entityRepository;
    /**
     * @var string
     */
    protected $repositoryFactoryFqn;
    /**
     * @var ClassMetadata|null
     */
    protected $metaData;
    /**
     * @var NamespaceHelper
     */
    protected $namespaceHelper;
    /**
     * @var EntityFactoryInterface
     */
    private $entityFactory;

    /**
     * AbstractEntityRepositoryFactory constructor.
     *
     * @param EntityManagerInterface $entityManager
     * @param EntityFactoryInterface $entityFactory
     * @param NamespaceHelper|null   $namespaceHelper
     */
    public function __construct(
        EntityManagerInterface $entityManager,
        EntityFactoryInterface $entityFactory,
        NamespaceHelper $namespaceHelper
    ) {
        $this->entityManager   = $entityManager;
        $this->namespaceHelper = $namespaceHelper;
        $this->entityFactory   = $entityFactory;
        $this->initRepository();
    }

    protected function initRepository(): void
    {
        if (null === $this->metaData) {
            $entityFqn      = $this->getEntityFqn();
            $this->metaData = $this->entityManager->getClassMetadata($entityFqn);
        }

        $this->entityRepository = new EntityRepository($this->entityManager, $this->metaData);
    }

    protected function getEntityFqn(): string
    {
        return '\\' . str_replace(
            [
                    'Entity\\Repositories',
                ],
            [
                    'Entities',
                ],
            $this->namespaceHelper->cropSuffix(static::class, 'Repository')
        );
    }

    public function getRandomResultFromQueryBuilder(QueryBuilder $queryBuilder, string $entityAlias): ?EntityInterface
    {
        $count = $this->getCountForQueryBuilder($queryBuilder, $entityAlias);
        if (0 === $count) {
            return null;
        }

        $queryBuilder->setMaxResults(1);
        $limitIndex = random_int(0, $count - 1);
        $results    = $queryBuilder->getQuery()
                                   ->setFirstResult($limitIndex)
                                   ->execute();
        $entity     = current($results);
        if (null === $entity) {
            return null;
        }
        $this->initialiseEntity($entity);

        return $entity;
    }

    public function getCountForQueryBuilder(QueryBuilder $queryBuilder, string $aliasToCount): int
    {
        $clone = clone $queryBuilder;
        $clone->select($queryBuilder->expr()->count($aliasToCount));

        return (int)$clone->getQuery()->getSingleScalarResult();
    }

    public function initialiseEntity(EntityInterface $entity)
    {
        $this->entityFactory->initialiseEntity($entity);

        return $entity;
    }

    /**
     * @return array|EntityInterface[]
     */
    public function findAll(): array
    {
        return $this->initialiseEntities($this->entityRepository->findAll());
    }

    public function initialiseEntities($entities)
    {
        foreach ($entities as $entity) {
            $this->initialiseEntity($entity);
        }

        return $entities;
    }

    /**
     * @param mixed    $id
     * @param int|null $lockMode
     * @param int|null $lockVersion
     *
     * @return EntityInterface
     * @throws DoctrineStaticMetaException
     */
    public function get($id, ?int $lockMode = null, ?int $lockVersion = null)
    {
        try {
            $entity = $this->find($id, $lockMode, $lockVersion);
        } catch (ConversionException $e) {
            $error = 'Failed getting by id ' . $id
                     . ', unless configured as an int ID entity, this should be a valid UUID';
            throw new DoctrineStaticMetaException($error, $e->getCode(), $e);
        }
        if ($entity === null) {
            throw new DoctrineStaticMetaException('Could not find the entity with id ' . $id);
        }

        return $this->initialiseEntity($entity);
    }

    /**
     * @param mixed    $id
     * @param int|null $lockMode
     * @param int|null $lockVersion
     *
     * @return EntityInterface|null
     */
    public function find($id, ?int $lockMode = null, ?int $lockVersion = null)
    {
        $entity = $this->entityRepository->find($id, $lockMode, $lockVersion);
        if (null === $entity) {
            return null;
        }
        if ($entity instanceof EntityInterface) {
            $this->initialiseEntity($entity);

            return $entity;
        }
    }

    /**
     * @param array      $criteria
     * @param array|null $orderBy
     *
     * @return EntityInterface
     */
    public function getOneBy(array $criteria, ?array $orderBy = null)
    {
        $result = $this->findOneBy($criteria, $orderBy);
        if ($result === null) {
            throw new RuntimeException('Could not find the entity');
        }

        return $this->initialiseEntity($result);
    }

    /**
     * @param array      $criteria
     * @param array|null $orderBy
     *
     * @return EntityInterface|null
     */
    public function findOneBy(array $criteria, ?array $orderBy = null)
    {
        $criteria = $this->mapCriteriaSetUuidsToStrings($criteria);
        $entity   = $this->entityRepository->findOneBy($criteria, $orderBy);
        if (null === $entity) {
            return null;
        }
        if ($entity instanceof EntityInterface) {
            $this->initialiseEntity($entity);

            return $entity;
        }
    }

    public function mapCriteriaSetUuidsToStrings(array $criteria): array
    {
        foreach ($criteria as $property => $value) {
            if ($value instanceof EntityInterface) {
                $criteria[$property] = $value->getId();
            }
            if ($value instanceof UuidInterface) {
                $criteria[$property] = $value->toString();
            }
        }

        return $criteria;
    }

    /**
     * @param array $criteria
     *
     * @return EntityInterface|null
     */
    public function getRandomOneBy(array $criteria)
    {
        $found = $this->getRandomBy($criteria);
        if ([] === $found) {
            throw new RuntimeException('Failed finding any Entities with this criteria');
        }
        $entity = current($found);
        if ($entity instanceof EntityInterface) {
            return $entity;
        }
        throw new RuntimeException('Unexpected Entity Type ' . get_class($entity));
    }

    /**
     * @param array $criteria
     *
     * @param int   $numToGet
     *
     * @return EntityInterface[]|array
     */
    public function getRandomBy(array $criteria, int $numToGet = 1): array
    {
        $count = $this->count($criteria);
        if (0 === $count) {
            return [];
        }
        $randOffset = rand(0, $count - $numToGet);

        return $this->findBy($criteria, null, $numToGet, $randOffset);
    }

    public function count(array $criteria = []): int
    {
        $criteria = $this->mapCriteriaSetUuidsToStrings($criteria);

        return $this->entityRepository->count($criteria);
    }

    /**
     * @param array      $criteria
     * @param array|null $orderBy
     * @param int|null   $limit
     * @param int|null   $offset
     *
     * @return array|EntityInterface[]
     */
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
    {
        $criteria = $this->mapCriteriaSetUuidsToStrings($criteria);

        return $this->initialiseEntities($this->entityRepository->findBy($criteria, $orderBy, $limit, $offset));
    }

    public function matching(Criteria $criteria): LazyCriteriaCollection
    {
        $collection = $this->entityRepository->matching($criteria);
        if ($collection instanceof LazyCriteriaCollection) {
            return $this->initialiseEntities($collection);
        }
    }

    public function createResultSetMappingBuilder(string $alias): Query\ResultSetMappingBuilder
    {
        return $this->entityRepository->createResultSetMappingBuilder($alias);
    }

    public function createNamedQuery(string $queryName): Query
    {
        return $this->entityRepository->createNamedQuery($queryName);
    }

    public function createNativeNamedQuery(string $queryName): NativeQuery
    {
        return $this->entityRepository->createNativeNamedQuery($queryName);
    }

    public function clear(): void
    {
        throw new \ErrorException(
            'Calling clear on entity repositories is deprecated as it tries to clear only specific entities ' .
            'which is not very well supported.' .
            ' If you want to clear the full unit of work, then use the EntityManager directly.',
            E_USER_DEPRECATED
        );
    }

    /**
     * Create a query builder with the alias preset
     *
     * @param string|null $indexBy
     *
     * @return QueryBuilder
     */
    public function createQueryBuilderWithAlias(string $indexBy = null): QueryBuilder
    {
        return $this->createQueryBuilder($this->getAlias(), $indexBy);
    }

    public function createQueryBuilder(string $alias, string $indexBy = null): QueryBuilder
    {
        return (new UuidQueryBuilder($this->entityManager))
            ->select($alias)
            ->from($this->getClassName(), $alias, $indexBy);
    }

    public function getClassName(): string
    {
        return $this->entityRepository->getClassName();
    }

    /**
     * Generate an alias based on the class name
     *
     * removes the words entity and repository
     *
     * gets all the upper case letters and returns them as a lower case string
     *
     * Warning - nothing is done to guarantee uniqueness for now
     *
     * @return string
     */
    public function getAlias(): string
    {
        if (isset(static::$aliasCache[static::class])) {
            return static::$aliasCache[static::class];
        }
        $class           = $this->namespaceHelper->getClassShortName($this->getClassName());
        $removeStopWords = str_ireplace(['entity', 'repository'], '', $class);
        $ucOnly          = preg_replace('%[^A-Z]%', '', $removeStopWords);

        static::$aliasCache[static::class] = strtolower($ucOnly);

        return static::$aliasCache[static::class];
    }

    public function createDeletionQueryBuilderWithAlias(): QueryBuilder
    {
        return $this->createDeletionQueryBuilder($this->getAlias());
    }

    public function createDeletionQueryBuilder(string $alias): QueryBuilder
    {
        return (new UuidQueryBuilder($this->entityManager))
            ->delete($this->getClassName(), $alias);
    }

    /**
     * For use with query builder, auto prefix alias
     *
     * @param string $property
     *
     * @return string
     */
    public function aliasPrefix(string $property): string
    {
        return $this->getAlias() . '.' . $property;
    }
}