src/Generate/RepositoriesGenerateCommand.php
<?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;
}
}