biurad/cycle-bridge

View on GitHub
src/Annotated/Entities.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

declare(strict_types=1);

/*
 * This file is part of Biurad opensource projects.
 *
 * PHP version 7.2 and above required
 *
 * @author    Divine Niiquaye Ibok <divineibok@gmail.com>
 * @copyright 2019 Biurad Group (https://biurad.com/)
 * @license   https://opensource.org/licenses/BSD-3-Clause License
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Biurad\Cycle\Annotated;

use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Configurator;
use Cycle\Annotated\Exception\AnnotationException;
use Cycle\Schema\Definition\Entity as EntitySchema;
use Cycle\Schema\Exception\RegistryException;
use Cycle\Schema\Exception\RelationException;
use Cycle\Schema\GeneratorInterface;
use Cycle\Schema\Registry;
use Doctrine\Common\Annotations\AnnotationException as DoctrineException;
use Doctrine\Common\Annotations\AnnotationReader;
use ReflectionClass;
use ReflectionException;

/**
 * Generates ORM schema based on annotated classes.
 */
final class Entities implements GeneratorInterface
{
    // table name generation
    public const TABLE_NAMING_PLURAL   = 1;

    public const TABLE_NAMING_SINGULAR = 2;

    public const TABLE_NAMING_NONE     = 3;

    /** @var class-string[] */
    private $locator;

    /** @var AnnotationReader */
    private $reader;

    /** @var Configurator */
    private $generator;

    /** @var int */
    private $tableNaming;

    /** @var \Doctrine\Inflector\Inflector */
    private $inflector;

    /**
     * @param class-string[]        $locator
     * @param null|AnnotationReader $reader
     * @param int                   $tableNaming
     */
    public function __construct(
        array $locator,
        AnnotationReader $reader = null,
        int $tableNaming = self::TABLE_NAMING_PLURAL
    ) {
        $this->locator     = $locator;
        $this->reader      = $reader ?? new AnnotationReader();
        $this->generator   = new Configurator($this->reader);
        $this->tableNaming = $tableNaming;
        $this->inflector   = (new \Doctrine\Inflector\Rules\English\InflectorFactory())->build();
    }

    /**
     * @param Registry $registry
     *
     * @return Registry
     */
    public function run(Registry $registry): Registry
    {
        /** @var EntitySchema[] $children */
        $children = [];

        foreach ($this->locator as $class) {
            try {
                $class = new ReflectionClass($class);

                /** @var Entity $ann */
                $ann = $this->reader->getClassAnnotation($class, Entity::class);
            } catch (DoctrineException $e) {
                throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
            }

            if ($ann === null) {
                continue;
            }

            $e = $this->generator->initEntity($ann, $class);

            // columns
            $this->generator->initFields($e, $class);

            // relations
            $this->generator->initRelations($e, $class);

            // additional columns (mapped to local fields automatically)
            $this->generator->initColumns($e, $ann->getColumns(), $class);

            if ($this->hasParent($registry, $e->getClass())) {
                $children[] = $e;

                continue;
            }

            // register entity (OR find parent)
            $registry->register($e);

            $registry->linkTable($e, $ann->getDatabase(), $ann->getTable() ?? $this->tableName($e->getRole()));
        }

        foreach ($children as $e) {
            $registry->registerChild($registry->getEntity($this->findParent($registry, $e->getClass())), $e);
        }

        return $this->normalizeNames($registry);
    }

    /**
     * @param Registry $registry
     *
     * @return Registry
     */
    protected function normalizeNames(Registry $registry): Registry
    {
        // resolve all the relation target names into roles
        foreach ($this->locator as $class) {
            $class = new ReflectionClass($class);

            if (!$registry->hasEntity($class->getName())) {
                continue;
            }

            $e = $registry->getEntity($class->getName());

            // relations
            foreach ($e->getRelations() as $name => $r) {
                try {
                    $r->setTarget($this->resolveTarget($registry, $r->getTarget()));

                    if ($r->getOptions()->has('though')) {
                        $r->getOptions()->set(
                            'though',
                            $this->resolveTarget($registry, $r->getOptions()->get('though'))
                        );
                    }
                } catch (RegistryException $ex) {
                    throw new RelationException(
                        \sprintf(
                            'Unable to resolve `%s`.`%s` relation target (not found or invalid)',
                            $e->getRole(),
                            $name
                        ),
                        $ex->getCode(),
                        $ex
                    );
                }
            }
        }

        return $registry;
    }

    /**
     * @param Registry $registry
     * @param string   $name
     *
     * @return null|string
     */
    protected function resolveTarget(Registry $registry, string $name): ?string
    {
        if (null === $name || \interface_exists($name, true)) {
            // do not resolve interfaces
            return $name;
        }

        if (!$registry->hasEntity($name)) {
            // point all relations to the parent
            foreach ($registry as $entity) {
                foreach ($registry->getChildren($entity) as $child) {
                    if ($child->getClass() === $name || $child->getRole() === $name) {
                        return $entity->getRole();
                    }
                }
            }
        }

        return $registry->getEntity($name)->getRole();
    }

    /**
     * @param string $role
     *
     * @return string
     */
    protected function tableName(string $role): string
    {
        $table = $this->inflector->tableize($role);

        switch ($this->tableNaming) {
            case self::TABLE_NAMING_PLURAL:
                return $this->inflector->pluralize($this->inflector->tableize($role));

            case self::TABLE_NAMING_SINGULAR:
                return $this->inflector->singularize($this->inflector->tableize($role));

            default:
                return $table;
        }
    }

    /**
     * @param Registry $registry
     * @param string   $class
     *
     * @return bool
     */
    protected function hasParent(Registry $registry, string $class): bool
    {
        return $this->findParent($registry, $class) !== null;
    }

    /**
     * @param Registry $registry
     * @param string   $class
     *
     * @return null|string
     */
    protected function findParent(Registry $registry, string $class): ?string
    {
        $parents = \class_parents($class);

        foreach (\array_reverse($parents) as $parent) {
            try {
                $class = new ReflectionClass($parent);
            } catch (ReflectionException $e) {
                continue;
            }

            if ($class->getDocComment() === false) {
                continue;
            }

            $ann = $this->reader->getClassAnnotation($class, Entity::class);

            if ($ann !== null) {
                return $parent;
            }
        }

        return null;
    }
}