edmondscommerce/doctrine-static-meta

View on GitHub
src/CodeGeneration/Generator/Field/ArchetypeFieldGenerator.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types=1);

namespace EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Field;

use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\CodeHelper;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\FindAndReplaceHelper;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\ReflectionHelper;
use ReflectionException;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem;
use ts\Reflection\ReflectionClass;

use function class_exists;
use function preg_replace;
use function str_replace;
use function substr;

/**
 * Class ArchetypeFieldGenerator
 *
 * @package  EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator
 * @SuppressWarnings(PHPMD.StaticAccess)
 * @internal - this is only accessed via CodeGeneration\Generator\Field\FieldGenerator
 */
class ArchetypeFieldGenerator
{
    public const ARCHETYPE_FAKER_DATA_PROVIDER_ALIAS = 'ArchetypeFakerDataProvider';

    /**
     * @var string
     */
    protected $fieldFqn;
    /**
     * @var string
     */
    protected $traitPath;
    /**
     * @var string
     */
    protected $interfacePath;
    /**
     * @var ReflectionClass
     */
    protected $archetypeFieldTrait;
    /**
     * @var ReflectionClass
     */
    protected $archetypeFieldInterface;
    /**
     * @var Filesystem
     */
    protected $filesystem;
    /**
     * @var NamespaceHelper
     */
    protected $namespaceHelper;
    /**
     * @var string
     */
    protected $projectRootNamespace;
    /**
     * @var CodeHelper
     */
    protected $codeHelper;
    /**
     * @var FindAndReplaceHelper
     */
    protected $findAndReplaceHelper;
    /**
     * @var ReflectionHelper
     */
    protected $reflectionHelper;

    /**
     * ArchetypeFieldGenerator constructor.
     *
     * @param Filesystem           $filesystem
     * @param NamespaceHelper      $namespaceHelper
     * @param CodeHelper           $codeHelper
     * @param FindAndReplaceHelper $findAndReplaceHelper
     * @param ReflectionHelper     $reflectionHelper
     */
    public function __construct(
        Filesystem $filesystem,
        NamespaceHelper $namespaceHelper,
        CodeHelper $codeHelper,
        FindAndReplaceHelper $findAndReplaceHelper,
        ReflectionHelper $reflectionHelper
    ) {
        $this->filesystem           = $filesystem;
        $this->namespaceHelper      = $namespaceHelper;
        $this->codeHelper           = $codeHelper;
        $this->findAndReplaceHelper = $findAndReplaceHelper;
        $this->reflectionHelper     = $reflectionHelper;
    }

    /**
     * @param string $fieldFqn
     * @param string $traitPath
     * @param string $interfacePath
     * @param string $archetypeFieldTraitFqn
     * @param string $projectRootNamespace
     *
     * @return string
     * @throws ReflectionException
     */
    public function createFromArchetype(
        string $fieldFqn,
        string $traitPath,
        string $interfacePath,
        string $archetypeFieldTraitFqn,
        string $projectRootNamespace
    ): string {
        $this->fieldFqn                = $fieldFqn;
        $this->traitPath               = $traitPath;
        $this->interfacePath           = $interfacePath;
        $this->archetypeFieldTrait     = new ReflectionClass($archetypeFieldTraitFqn);
        $this->archetypeFieldInterface = $this->getArchetypeInterfaceReflection();
        $this->projectRootNamespace    = $projectRootNamespace;
        $this->copyTrait();
        $this->copyInterface();
        $this->copyFakerProvider();

        return $this->fieldFqn;
    }

    private function getArchetypeInterfaceReflection(): ReflectionClass
    {
        $interfaceFqn = str_replace(
            '\\Fields\\Traits\\',
            '\\Fields\\Interfaces\\',
            $this->namespaceHelper->cropSuffix(
                $this->archetypeFieldTrait->getName(),
                'Trait'
            ) . 'Interface'
        );

        return new ReflectionClass($interfaceFqn);
    }

    protected function copyTrait(): void
    {
        $this->filesystem->copy($this->archetypeFieldTrait->getFileName(), $this->traitPath);
        $this->replaceInPath($this->traitPath);
    }

    protected function replaceInPath(string $path): void
    {
        $contents              = \ts\file_get_contents($path);
        $archetypePropertyName = $this->getPropertyName($this->archetypeFieldTrait->getShortName());
        $fieldPropertyName     = $this->getPropertyName($this->namespaceHelper->getClassShortName($this->fieldFqn));
        $find                  = [
            '%(namespace|use) +?' . $this->findAndReplaceHelper->escapeSlashesForRegex($this->getArchetypeFqnRoot())
            . '(?!\\\\FakerData\\\\Abstract)%',
            '%' . $this->findAndReplaceHelper->escapeSlashesForRegex($this->getArchetypeSubNamespace()) . '%',
            '%(?<!new )(?<!Constraints\\\\)' . $this->codeHelper->classy($archetypePropertyName) . '%',
            '%' . $this->codeHelper->consty($archetypePropertyName) . '%',
            '%' . $this->codeHelper->propertyIsh($archetypePropertyName) . '%',
            '%isIs%',
        ];
        $replace               = [
            '$1 ' . $this->namespaceHelper->tidy($this->projectRootNamespace . '\\Entity\\Fields'),
            $this->getNewFqnSubNamespace(),
            $this->codeHelper->classy($fieldPropertyName),
            $this->codeHelper->consty($fieldPropertyName),
            $this->codeHelper->propertyIsh($fieldPropertyName),
            'is',
        ];

        $replaced = preg_replace($find, $replace, $contents);
        file_put_contents($path, $replaced);
    }

    protected function getPropertyName(string $fieldTraitFqn): string
    {
        return $this->namespaceHelper->cropSuffix(
            $fieldTraitFqn,
            'FieldTrait'
        );
    }

    private function getArchetypeFqnRoot(): string
    {
        return substr(
            $this->archetypeFieldInterface->getNamespaceName(),
            0,
            \ts\strpos($this->archetypeFieldInterface->getNamespaceName(), '\\Entity\\Fields\\Interfaces')
        ) . '\\Entity\\Fields';
    }

    private function getArchetypeSubNamespace(): string
    {
        $archetypeTraitFqn = $this->archetypeFieldTrait->getName();
        switch (true) {
            case \ts\stringContains($archetypeTraitFqn, 'EdmondsCommerce\\DoctrineStaticMeta'):
                $archetypeRootNs = 'EdmondsCommerce\\DoctrineStaticMeta';
                break;
            case \ts\stringContains($archetypeTraitFqn, $this->projectRootNamespace):
                $archetypeRootNs = $this->projectRootNamespace;
                break;
            default:
                throw new RuntimeException('Failed finding the archetype root NS in ' . __METHOD__);
        }
        list($className, , $subDirectories) = $this->namespaceHelper->parseFullyQualifiedName(
            $archetypeTraitFqn,
            'src',
            $archetypeRootNs
        );
        array_shift($subDirectories);
        $subNamespaceParts = [];
        foreach ($subDirectories as $subDirectory) {
            if ($subDirectory === $className) {
                break;
            }
            if ('Traits' === $subDirectory) {
                $subDirectory = '(Traits|Interfaces|FakerData)';
            }
            $subNamespaceParts[] = $subDirectory;
        }

        return implode('\\', $subNamespaceParts);
    }

    private function getNewFqnSubNamespace(): string
    {
        list(, , $subDirectories) = $this->namespaceHelper->parseFullyQualifiedName(
            $this->fieldFqn,
            'src',
            $this->projectRootNamespace
        );
        array_shift($subDirectories);
        $subNamespaceParts = [];
        foreach ($subDirectories as $subDirectory) {
            if ('Traits' === $subDirectory) {
                $subDirectory = '\$1';
            }
            $subNamespaceParts[] = $subDirectory;
        }

        return implode('\\', $subNamespaceParts);
    }

    protected function copyInterface(): void
    {
        $this->filesystem->copy($this->archetypeFieldInterface->getFileName(), $this->interfacePath);
        $this->replaceInPath($this->interfacePath);
    }

    protected function copyFakerProvider(): void
    {
        $archetypeFakerFqn = $this->reflectionHelper
            ->getFakerProviderFqnFromFieldTraitReflection($this->archetypeFieldTrait);
        if (!class_exists($archetypeFakerFqn)) {
            return;
        }
        $archetypeFaker = new ReflectionClass($archetypeFakerFqn);
        $newFakerPath   = str_replace(
            [
                '/Traits/',
                'FieldTrait',
            ],
            [
                '/FakerData/',
                'FakerData',
            ],
            $this->traitPath
        );
        $this->filesystem->copy($archetypeFaker->getFileName(), $newFakerPath);
        $this->replaceInPath($newFakerPath);
    }
}