edmondscommerce/doctrine-static-meta

View on GitHub
src/Builder/Builder.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

declare(strict_types=1);

namespace EdmondsCommerce\DoctrineStaticMeta\Builder;

use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Action\CreateDtosForAllEntitiesAction;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\CodeHelper;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Embeddable\ArchetypeEmbeddableGenerator;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Embeddable\EntityEmbeddableSetter;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\EntityGenerator;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Field\EntityFieldSetter;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Field\FieldGenerator;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\PostProcessor\CopyPhpstormMeta;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\PostProcessor\EntityFormatter;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\UnusedRelationsRemover;
use EdmondsCommerce\DoctrineStaticMeta\Config;
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
use Exception;
use gossi\codegen\model\PhpClass;
use gossi\codegen\model\PhpConstant;
use gossi\codegen\model\PhpInterface;
use gossi\codegen\model\PhpTrait;
use ReflectionException;
use RuntimeException;
use ts\Reflection\ReflectionClass;

use function preg_replace;

/**
 * Class Builder
 *
 * @package EdmondsCommerce\DoctrineStaticMeta\Builder
 * @SuppressWarnings(PHPMD)
 */
class Builder
{

    /**
     * @var EntityGenerator
     */
    protected $entityGenerator;
    /**
     * @var FieldGenerator
     */
    protected $fieldGenerator;
    /**
     * @var EntityFieldSetter
     */
    protected $fieldSetter;
    /**
     * @var RelationsGenerator
     */
    protected $relationsGenerator;
    /**
     * @var ArchetypeEmbeddableGenerator
     */
    protected $archetypeEmbeddableGenerator;
    /**
     * @var EntityEmbeddableSetter
     */
    protected $embeddableSetter;
    /**
     * @var CodeHelper
     */
    protected $codeHelper;
    /**
     * @var UnusedRelationsRemover
     */
    protected $unusedRelationsRemover;
    /**
     * @var CreateDtosForAllEntitiesAction
     */
    private $dataTransferObjectsForAllEntitiesAction;
    /**
     * @var EntityFormatter
     */
    private $entityFormatter;
    /**
     * @var CopyPhpstormMeta
     */
    private $copyPhpstormMeta;
    /**
     * @var NamespaceHelper
     */
    private $namespaceHelper;

    public function __construct(
        EntityGenerator $entityGenerator,
        FieldGenerator $fieldGenerator,
        EntityFieldSetter $fieldSetter,
        RelationsGenerator $relationsGenerator,
        ArchetypeEmbeddableGenerator $archetypeEmbeddableGenerator,
        EntityEmbeddableSetter $embeddableSetter,
        CodeHelper $codeHelper,
        UnusedRelationsRemover $unusedRelationsRemover,
        CreateDtosForAllEntitiesAction $dataTransferObjectsForAllEntitiesAction,
        EntityFormatter $entityFormatter,
        Config $config,
        CopyPhpstormMeta $copyPhpstormMeta,
        NamespaceHelper $namespaceHelper
    ) {
        $this->entityGenerator                         = $entityGenerator;
        $this->fieldGenerator                          = $fieldGenerator;
        $this->fieldSetter                             = $fieldSetter;
        $this->relationsGenerator                      = $relationsGenerator;
        $this->archetypeEmbeddableGenerator            = $archetypeEmbeddableGenerator;
        $this->embeddableSetter                        = $embeddableSetter;
        $this->codeHelper                              = $codeHelper;
        $this->unusedRelationsRemover                  = $unusedRelationsRemover;
        $this->dataTransferObjectsForAllEntitiesAction = $dataTransferObjectsForAllEntitiesAction;
        $this->entityFormatter                         = $entityFormatter;
        $this->copyPhpstormMeta                        = $copyPhpstormMeta;
        $this->namespaceHelper                         = $namespaceHelper;

        $this->setPathToProjectRoot($config::getProjectRootDirectory());
    }

    public function setPathToProjectRoot(string $pathToProjectRoot): self
    {
        $this->entityGenerator->setPathToProjectRoot($pathToProjectRoot);
        $this->fieldGenerator->setPathToProjectRoot($pathToProjectRoot);
        $this->fieldSetter->setPathToProjectRoot($pathToProjectRoot);
        $this->relationsGenerator->setPathToProjectRoot($pathToProjectRoot);
        $this->archetypeEmbeddableGenerator->setPathToProjectRoot($pathToProjectRoot);
        $this->unusedRelationsRemover->setPathToProjectRoot($pathToProjectRoot);
        $this->dataTransferObjectsForAllEntitiesAction->setProjectRootDirectory($pathToProjectRoot);
        $this->embeddableSetter->setPathToProjectRoot($pathToProjectRoot);
        $this->entityFormatter->setPathToProjectRoot($pathToProjectRoot);
        $this->copyPhpstormMeta->setPathToProjectRoot($pathToProjectRoot);

        return $this;
    }

    /**
     * @return EntityGenerator
     */
    public function getEntityGenerator(): EntityGenerator
    {
        return $this->entityGenerator;
    }

    /**
     * @return FieldGenerator
     */
    public function getFieldGenerator(): FieldGenerator
    {
        return $this->fieldGenerator;
    }

    /**
     * @return EntityFieldSetter
     */
    public function getFieldSetter(): EntityFieldSetter
    {
        return $this->fieldSetter;
    }

    /**
     * @return RelationsGenerator
     */
    public function getRelationsGenerator(): RelationsGenerator
    {
        return $this->relationsGenerator;
    }

    /**
     * @return ArchetypeEmbeddableGenerator
     */
    public function getArchetypeEmbeddableGenerator(): ArchetypeEmbeddableGenerator
    {
        return $this->archetypeEmbeddableGenerator;
    }

    /**
     * @return EntityEmbeddableSetter
     */
    public function getEmbeddableSetter(): EntityEmbeddableSetter
    {
        return $this->embeddableSetter;
    }

    /**
     * Finalise build - run various steps to wrap up the build and tidy up the codebase
     *
     * @return Builder
     */
    public function finaliseBuild(): self
    {
        $this->dataTransferObjectsForAllEntitiesAction->run();
        $this->entityFormatter->run();
        $this->copyPhpstormMeta->run();

        return $this;
    }

    /**
     * This step will remove any relations code that is not being used
     *
     * Generally it needs to be run in a separate PHP process to ensure PHP loads the final versions of code
     */
    public function removeUnusedRelations(): void
    {
        $this->unusedRelationsRemover->run();
    }

    /**
     * @param array $entityFqns
     *
     * @return Builder
     * @throws DoctrineStaticMetaException
     */
    public function generateEntities(array $entityFqns): self
    {
        $this->setProjectRootNamespace(
            $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn(
                current($entityFqns)
            )
        );
        foreach ($entityFqns as $entityFqn) {
            $this->entityGenerator->generateEntity($entityFqn);
        }

        return $this;
    }

    public function setProjectRootNamespace(string $projectRootNamespace): self
    {
        $this->entityGenerator->setProjectRootNamespace($projectRootNamespace);
        $this->fieldGenerator->setProjectRootNamespace($projectRootNamespace);
        $this->fieldSetter->setProjectRootNamespace($projectRootNamespace);
        $this->relationsGenerator->setProjectRootNamespace($projectRootNamespace);
        $this->archetypeEmbeddableGenerator->setProjectRootNamespace($projectRootNamespace);
        $this->dataTransferObjectsForAllEntitiesAction->setProjectRootNamespace($projectRootNamespace);
        $this->embeddableSetter->setProjectRootNamespace($projectRootNamespace);
        $this->unusedRelationsRemover->setProjectRootNamespace($projectRootNamespace);
        return $this;
    }

    /**
     * @param array $entityRelationEntity
     *
     * @return Builder
     * @throws DoctrineStaticMetaException
     */
    public function setEntityRelations(array $entityRelationEntity): self
    {
        foreach ($entityRelationEntity as list($owningEntityFqn, $hasType, $ownedEntityFqn)) {
            $this->relationsGenerator->setEntityHasRelationToEntity($owningEntityFqn, $hasType, $ownedEntityFqn);
        }

        return $this;
    }

    /**
     * @param array $fields
     *
     * @return array $traitFqns
     */
    public function generateFields(array $fields): array
    {
        $traitFqns = [];
        foreach ($fields as list($fieldFqn, $fieldType)) {
            try {
                $traitFqns[] = $this->fieldGenerator->generateField($fieldFqn, $fieldType);
            } catch (Exception $e) {
                throw new RuntimeException(
                    'Failed building field with $fieldFqn: ' . $fieldFqn . ' and $fieldType ' . $fieldType,
                    $e->getCode(),
                    $e
                );
            }
        }

        return $traitFqns;
    }

    public function generateKeyedFields(array $fields): array
    {
        $traitFqns = [];

        $defaults = [
            FieldGenerator::FIELD_PHP_TYPE_KEY      => null,
            FieldGenerator::FIELD_DEFAULT_VAULE_KEY => null,
            FieldGenerator::FIELD_IS_UNIQUE_KEY     => false,
        ];

        foreach ($fields as $field) {
            /* Can not use list here as it breaks PHPMD */
            $combinedDefaults = $field + $defaults;
            $fieldFqn         = $combinedDefaults[FieldGenerator::FIELD_FQN_KEY];
            $fieldType        = $combinedDefaults[FieldGenerator::FIELD_TYPE_KEY];
            $phpType          = $combinedDefaults[FieldGenerator::FIELD_PHP_TYPE_KEY];
            $defaultValue     = $combinedDefaults[FieldGenerator::FIELD_DEFAULT_VAULE_KEY];
            $isUnique         = $combinedDefaults[FieldGenerator::FIELD_IS_UNIQUE_KEY];
            try {
                $traitFqns[] =
                    $this->fieldGenerator->generateField($fieldFqn, $fieldType, $phpType, $defaultValue, $isUnique);
            } catch (Exception $e) {
                throw new RuntimeException(
                    'Failed building field with $fieldFqn: ' . $fieldFqn . ' and $fieldType ' . $fieldType,
                    $e->getCode(),
                    $e
                );
            }
        }

        return $traitFqns;
    }

    /**
     * @param string $entityFqn
     * @param array  $fieldFqns
     *
     * @return Builder
     * @throws DoctrineStaticMetaException
     */
    public function setFieldsToEntity(string $entityFqn, array $fieldFqns): self
    {
        foreach ($fieldFqns as $fieldFqn) {
            $this->fieldSetter->setEntityHasField($entityFqn, $fieldFqn);
        }

        return $this;
    }

    /**
     * @param array $embeddables
     *
     * @return array $traitFqns
     * @throws DoctrineStaticMetaException
     * @throws ReflectionException
     */
    public function generateEmbeddables(array $embeddables): array
    {
        $traitFqns = [];
        foreach ($embeddables as $embeddable) {
            [$archetypeEmbeddableObjectFqn, $newEmbeddableObjectClassName] = array_values($embeddable);
            $traitFqns[] = $this->archetypeEmbeddableGenerator->createFromArchetype(
                $archetypeEmbeddableObjectFqn,
                $newEmbeddableObjectClassName
            );
        }

        return $traitFqns;
    }

    /**
     * @param string $entityFqn
     * @param array  $embeddableTraitFqns
     *
     * @return Builder
     */
    public function setEmbeddablesToEntity(string $entityFqn, array $embeddableTraitFqns): self
    {
        foreach ($embeddableTraitFqns as $embeddableTraitFqn) {
            $this->embeddableSetter->setEntityHasEmbeddable($entityFqn, $embeddableTraitFqn);
        }

        return $this;
    }

    public function setEnumOptionsOnInterface(string $interfaceFqn, array $options): void
    {
        $pathToInterface = (new ReflectionClass($interfaceFqn))->getFileName();
        $basename        = basename($pathToInterface);
        $classy          = substr($basename, 0, strpos($basename, 'FieldInterface'));
        $consty          = $this->codeHelper->consty($classy);
        $interface       = PhpInterface::fromFile($pathToInterface);
        $constants       = $interface->getConstants();
        $constants->map(static function (PhpConstant $constant) use ($interface, $consty) {
            if (0 === strpos($constant->getName(), $consty . '_OPTION')) {
                $interface->removeConstant($constant);
            }
            if (0 === strpos($constant->getName(), 'DEFAULT')) {
                $interface->removeConstant($constant);
            }
        });
        $optionConsts = [];
        foreach ($options as $option) {
            $name           = preg_replace(
                '%_{2,}%',
                '_',
                $consty . '_OPTION_' . $this->codeHelper->consty(
                    preg_replace('%[^a-z0-9]%i', '_', $option)
                )
            );
            $optionConsts[] = 'self::' . $name;
            $constant       = new PhpConstant($name, $option);
            $interface->setConstant($constant);
        }
        $interface->setConstant(
            new PhpConstant(
                $consty . '_OPTIONS',
                '[' . implode(",\n", $optionConsts) . ']',
                true
            )
        );
        $interface->setConstant(
            new PhpConstant(
                'DEFAULT_' . $consty,
                current($optionConsts),
                true
            )
        );
        $this->codeHelper->generate($interface, $pathToInterface);
    }

    public function injectTraitInToClass(string $traitFqn, string $classFqn): void
    {
        $classFilePath = $this->getFileName($classFqn);
        $class         = PhpClass::fromFile($classFilePath);
        $trait         = PhpTrait::fromFile($this->getFileName($traitFqn));
        $traits        = $class->getTraits();
        $exists        = array_search($traitFqn, $traits, true);
        if ($exists !== false) {
            return;
        }
        $class->addTrait($trait);
        $this->codeHelper->generate($class, $classFilePath);
    }

    private function getFileName(string $typeFqn): string
    {
        $reflectionClass = new ReflectionClass($typeFqn);

        return $reflectionClass->getFileName();
    }

    public function extendInterfaceWithInterface(string $interfaceToExtendFqn, string $interfaceToAddFqn): void
    {
        $toExtendFilePath = $this->getFileName($interfaceToExtendFqn);
        $toExtend         = PhpInterface::fromFile($toExtendFilePath);
        $toAdd            = PhpInterface::fromFile($this->getFileName($interfaceToAddFqn));
        $exists           = $toExtend->getInterfaces()->contains($interfaceToAddFqn);
        if ($exists !== false) {
            return;
        }
        $toExtend->addInterface($toAdd);
        $this->codeHelper->generate($toExtend, $toExtendFilePath);
    }

    public function removeIdTraitFromClass(string $classFqn): void
    {
        $traitFqn = "DSM\\Fields\\Traits\\PrimaryKey\\IdFieldTrait";
        $this->removeTraitFromClass($classFqn, $traitFqn);
    }

    public function removeTraitFromClass(string $classFqn, string $traitFqn): void
    {
        $classPath = $this->getFileName($classFqn);
        $class     = PhpClass::fromFile($classPath);
        $traits    = $class->getTraits();
        if ($class->getUseStatements()->contains($traitFqn) === true) {
            $class->removeUseStatement($traitFqn);
        }
        $index = array_search($traitFqn, $traits, true);
        if ($index === false) {
            $shortNameParts = explode('\\', $traitFqn);
            $shortName      = (string)array_pop($shortNameParts);
            $index          = array_search($shortName, $traits, true);
        }
        if ($index === false) {
            return;
        }
        unset($traits[$index]);
        $reflectionClass = new ReflectionClass(PhpClass::class);
        $property        = $reflectionClass->getProperty('traits');
        $property->setAccessible(true);
        $property->setValue($class, $traits);
        $this->codeHelper->generate($class, $classPath);
    }
}