squirrelphp/entities

View on GitHub
src/Generate/RepositoriesGenerateCommand.php

Summary

Maintainability
C
7 hrs
Test Coverage
A
100%
<?php

namespace Squirrel\Entities\Generate;

use Squirrel\Entities\Attribute\EntityProcessor;

/**
 * Generate repositories and service definitions for SQLMapper entities
 */
class RepositoriesGenerateCommand
{
    private FindClassesWithAttribute $findClassesWithAttribute;
    private array $repositoryPhpFileBlueprint = [
        'ReadOnly' => <<<'EOD'
<?php
// phpcs:ignoreFile -- created by SquirrelPHP entities library, do not alter
/*
 * THIS FILE IS AUTOMATICALLY CREATED - DO NOT EDIT, DO NOT COMMIT TO VCS
 *
 * IF YOU DELETE THE ENTITY ({namespaceOfEntity}\{classOfEntity})
 * THEN PLEASE DELETE THIS FILE - IT WILL NO LONGER BE NEEDED
 *
 * Generated by Squirrel\Entities\Generate\RepositoriesGenerateCommand,
 * this file will be overwritten when that command is executed again, if your
 * entity still exists at that time
 */
// @codeCoverageIgnoreStart

namespace {namespaceOfBuilders} {
    /**
     * @implements \Iterator<int,\{namespaceOfEntity}\{classOfEntity}>
     */
    class SelectIterator extends \Squirrel\Entities\Builder\SelectIterator implements \Iterator
    {
        public function current(): \{namespaceOfEntity}\{classOfEntity}
        {
            $entry = parent::current();

            if ($entry instanceof \{namespaceOfEntity}\{classOfEntity}) {
                return $entry;
            }

            throw new \LogicException('Unexpected type encountered - wrong repository might be configured: ' . \get_class($entry));
        }
    }

    /**
     * This class exists to have proper type hints about the object(s) returned in the
     * getEntries and getOneEntry functions. The heavy lifting is done by the
     * SelectEntries class
     *
     * @implements \IteratorAggregate<int,\{namespaceOfEntity}\{classOfEntity}>
     */
    class SelectEntries extends \Squirrel\Entities\Builder\SelectEntries implements \IteratorAggregate
    {
        /**
         * @return \{namespaceOfEntity}\{classOfEntity}[]
         */
        public function getAllEntries(): array
        {
            /** @var \{namespaceOfEntity}\{classOfEntity}[] $entries */
            $entries = parent::getAllEntries();

            foreach ($entries as $entry) {
                if (!($entry instanceof \{namespaceOfEntity}\{classOfEntity})) {
                    throw new \LogicException('Unexpected type encountered - wrong repository might be configured: ' . \get_class($entry));
                }
            }

            return $entries;
        }

        public function getOneEntry(): ?\{namespaceOfEntity}\{classOfEntity}
        {
            $entry = parent::getOneEntry();

            if ($entry instanceof \{namespaceOfEntity}\{classOfEntity} || $entry === null) {
                return $entry;
            }

            throw new \LogicException('Unexpected type encountered - wrong repository might be configured: ' . \get_class($entry));
        }

        public function getIterator(): SelectIterator
        {
            return new SelectIterator($this->repository, [
                'where' => $this->where,
                'order' => $this->orderBy,
                'fields' => $this->fields,
                'limit' => $this->limitTo,
                'offset' => $this->startAt,
                'lock' => $this->blocking,
            ]);
        }
    }
}

namespace {namespaceOfEntity} {
    use Squirrel\Entities\RepositoryBuilderReadOnlyInterface;
    use Squirrel\Entities\RepositoryReadOnlyInterface;

    class {classOfEntity}RepositoryReadOnly implements RepositoryBuilderReadOnlyInterface
    {
        public function __construct(private RepositoryReadOnlyInterface $repository)
        {
        }

        public function count(): \Squirrel\Entities\Builder\CountEntries
        {
            return new \Squirrel\Entities\Builder\CountEntries($this->repository);
        }

        public function select(): \{namespaceOfBuilders}\SelectEntries
        {
            return new \{namespaceOfBuilders}\SelectEntries($this->repository);
        }
    }
}
// @codeCoverageIgnoreEnd

EOD
        ,
        'Writeable' => <<<'EOD'
<?php
// phpcs:ignoreFile -- created by SquirrelPHP entities library, do not alter
/*
 * THIS FILE IS AUTOMATICALLY CREATED - DO NOT EDIT, DO NOT COMMIT TO VCS
 *
 * IF YOU DELETE THE ENTITY ({namespaceOfEntity}\{classOfEntity})
 * THEN PLEASE DELETE THIS FILE - IT WILL NO LONGER BE NEEDED
 *
 * Generated by Squirrel\Entities\Generate\RepositoriesGenerateCommand,
 * this file will be overwritten when that command is executed again, if your
 * entity still exists at that time
 */
// @codeCoverageIgnoreStart

namespace {namespaceOfEntity} {
    use Squirrel\Entities\RepositoryBuilderWriteableInterface;
    use Squirrel\Entities\RepositoryWriteableInterface;

    class {classOfEntity}RepositoryWriteable extends {classOfEntity}RepositoryReadOnly implements
        RepositoryBuilderWriteableInterface
    {
        public function __construct(private RepositoryWriteableInterface $repository)
        {
            parent::__construct($repository);
        }

        public function insert(): \Squirrel\Entities\Builder\InsertEntry
        {
            return new \Squirrel\Entities\Builder\InsertEntry($this->repository);
        }

        public function insertOrUpdate(): \Squirrel\Entities\Builder\InsertOrUpdateEntry
        {
            return new \Squirrel\Entities\Builder\InsertOrUpdateEntry($this->repository);
        }

        public function update(): \Squirrel\Entities\Builder\UpdateEntries
        {
            return new \Squirrel\Entities\Builder\UpdateEntries($this->repository);
        }

        public function delete(): \Squirrel\Entities\Builder\DeleteEntries
        {
            return new \Squirrel\Entities\Builder\DeleteEntries($this->repository);
        }
    }
}
// @codeCoverageIgnoreEnd

EOD
        ,
    ];

    public function __construct(
        /** @var string[] */
        private array $sourceCodeDirectories,
        private bool $forceFileCreation,
        private PHPFilesInDirectoryGetContents $PHPFilesInDirectoryGetContents,
    ) {
        $this->findClassesWithAttribute = new FindClassesWithAttribute();
    }

    /**
     * @return array{list<string>, list<string>}
     */
    public function __invoke(): array
    {
        /** @var list<string> $logRepositories */
        $logRepositories = [];
        /** @var list<string> $logConflicts */
        $logConflicts = [];

        // Initialize entity processor to find repository config
        $entityProcessor = new EntityProcessor();

        /** @var array<string, string> $filesToCreate */
        $filesToCreate = [];

        // Saves the files per path for which to create a .gitignore file
        $gitignoreFilesForPaths = [];

        // Go through directories
        foreach ($this->sourceCodeDirectories as $directory) {
            // Go through files which were found
            foreach (($this->PHPFilesInDirectoryGetContents)($directory) as $fileData) {
                // Get all possible entity classes with our attribute
                $classes = $this->findClassesWithAttribute->__invoke($fileData['contents']);

                // Go through the possible entity classes
                foreach ($classes as [$namespace, $className]) {
                    /** @var class-string $fullClassName */
                    $fullClassName = $namespace . '\\' . $className;

                    // Get repository config as object from attributes
                    $repositoryConfig = $entityProcessor->process($fullClassName);

                    // Repository config found - this is definitely an entity
                    if (isset($repositoryConfig)) {
                        $logRepositories[] = 'Entity found: ' . $fullClassName;

                        foreach (['ReadOnly', 'Writeable'] as $type) {
                            $phpFilename = $this->generateRepositoryFilename($fileData['filename'], $type);

                            $filesToCreate[$fileData['path'] . '/' . $phpFilename] = $this->repositoryFileContentsFillInBlueprint(
                                $this->repositoryPhpFileBlueprint[$type],
                                $namespace,
                                $className,
                            );

                            $gitignoreFilesForPaths[$fileData['path']][] = $phpFilename;
                        }
                    }
                }
            }
        }

        if (\count($gitignoreFilesForPaths) > 0) {
            foreach ($gitignoreFilesForPaths as $path => $files) {
                $filesToCreate[$path . '/.gitignore'] = $this->generateGitignoreContents($files);
            }
        }

        $conflictDetected = false;

        foreach ($filesToCreate as $fullFilename => $fileContents) {
            if (\file_exists($fullFilename)) {
                $currentContents = \file_get_contents($fullFilename);

                // @codeCoverageIgnoreStart
                if ($currentContents === false) {
                    throw new \RuntimeException('Cannot read file: ' . $fullFilename);
                }
                // @codeCoverageIgnoreEnd

                // Only assume no conflict if we have an explicit generated by string in the file
                if (!\str_contains($currentContents, 'Generated by Squirrel\Entities\Generate\RepositoriesGenerateCommand')) {
                    $conflictDetected = true;

                    $logConflicts[] = 'Possible conflict detected for: ' . $fullFilename;
                }

                // Remove files which exist in the exact same way already
                if ($currentContents === $fileContents) {
                    unset($filesToCreate[$fullFilename]);
                }
            }
        }

        if ($conflictDetected === false || $this->forceFileCreation === true) {
            foreach ($filesToCreate as $fullFilename => $fileContents) {
                \file_put_contents($fullFilename, $fileContents);
            }
        }

        return [$logRepositories, $logConflicts];
    }

    private function generateGitignoreContents(array $files): string
    {
        // Make sure all files are unique / no duplicates
        $files = \array_unique($files);

        // Ignore the .gitignore file in entity directories, that way both the gitignore
        // and the repositories will be ignored by VCS
        $gitignoreLines = [
            '# Generated by Squirrel\Entities\Generate\RepositoriesGenerateCommand',
            '# Any changes will be overwritten when running that command again',
            '# => DO NOT EDIT, DO NOT COMMIT TO VCS',
            '.gitignore',
        ];

        // Add each repository file to .gitignore
        foreach ($files as $filename) {
            $gitignoreLines[] = $filename;
        }

        return \implode("\n", $gitignoreLines);
    }

    private function generateRepositoryFilename(string $filename, string $type): string
    {
        return \str_replace('.php', '', $filename) . 'Repository' . $type . '.php';
    }

    private function repositoryFileContentsFillInBlueprint(
        string $repositoryPhpFile,
        string $namespace,
        string $className,
    ): string {
        $fullClassnameWithoutSeparator = \str_replace(
            '\\',
            '',
            $namespace . $className,
        );
        $repositoryPhpFile = \str_replace(
            '{namespaceOfEntity}',
            $namespace,
            $repositoryPhpFile,
        );
        $repositoryPhpFile = \str_replace(
            '{namespaceOfBuilders}',
            'Squirrel\\Entities\\Builder\\' . $fullClassnameWithoutSeparator,
            $repositoryPhpFile,
        );
        $repositoryPhpFile = \str_replace(
            '{classOfEntity}',
            $className,
            $repositoryPhpFile,
        );
        return $repositoryPhpFile;
    }
}