src/DependencyInjection/SquirrelEntitiesExtension.php
<?php
namespace Squirrel\EntitiesBundle\DependencyInjection;
use Squirrel\Entities\Attribute\EntityProcessor;
use Squirrel\Entities\Generate\FindClassesWithAttribute;
use Squirrel\Entities\MultiRepositoryBuilderReadOnly;
use Squirrel\Entities\MultiRepositoryBuilderReadOnlyInterface;
use Squirrel\Entities\MultiRepositoryBuilderWriteable;
use Squirrel\Entities\MultiRepositoryBuilderWriteableInterface;
use Squirrel\Entities\MultiRepositoryReadOnly;
use Squirrel\Entities\MultiRepositoryReadOnlyInterface;
use Squirrel\Entities\MultiRepositoryWriteable;
use Squirrel\Entities\MultiRepositoryWriteableInterface;
use Squirrel\Entities\RepositoryConfig;
use Squirrel\Entities\RepositoryConfigInterface;
use Squirrel\Entities\RepositoryReadOnly;
use Squirrel\Entities\RepositoryWriteable;
use Squirrel\Entities\Transaction;
use Squirrel\Entities\TransactionInterface;
use Squirrel\Queries\DBInterface;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Finder\Finder;
/**
* Loads bundle configuration and auto-configures repositories as services
*/
class SquirrelEntitiesExtension extends Extension
{
public function __construct(
private EntityProcessor $entityProcessor,
private FindClassesWithAttribute $identifyEntityClasses,
) {
}
public function load(array $configs, ContainerBuilder $container): void
{
/**
* @var Configuration $configuration
*/
$configuration = $this->getConfiguration([], $container);
$config = $this->processConfiguration($configuration, $configs);
// Looks for entities so we can add their repositories and create transaction services
$this->findEntitiesAndProcess($container, $config);
// Add multi repositories services to container
$this->createMultiRepositoryServices($container);
}
public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface
{
return new Configuration($this->getAlias());
}
public function getAlias(): string
{
return 'squirrel_entities';
}
private function findEntitiesAndProcess(ContainerBuilder $container, ?array $config): void
{
// No directories defined - this is mandatory
if (!isset($config) || \count($config['directories'] ?? []) === 0) {
return;
}
// Collect used connections - to create transaction services further down
$connectionNames = [];
// Get all possible entity classes with our annotation
$classes = $this->findAllEntityClassesInFilesystem($config['directories']);
// Go through the possible entity classes
foreach ($classes as $class) {
// Divvy up the namespace and the class name
$namespace = $class[0];
$className = $class[1];
/**
* @psalm-var class-string $fullClassName
*/
$fullClassName = $namespace . '\\' . $className;
// Get repository config as object
$repositoryConfig = $this->entityProcessor->process($fullClassName);
// Repository config found - this is an entity
if (isset($repositoryConfig)) {
$connectionNames[] = $this->createRepositoryServicesForEntity(
$container,
$repositoryConfig,
$config['connection_names'][$fullClassName] ?? null,
$config['table_names'][$fullClassName] ?? null,
);
}
}
// Create a transaction service for each connection
$this->createTransactionServices($container, $connectionNames);
}
private function findAllEntityClassesInFilesystem(array $directories): array
{
$entityClasses = [];
// Go through directories
foreach ($directories as $directory) {
// Go through files which were found
foreach ($this->findNextFileAndReturnContentsGenerator($directory) as $fileContents) {
// Get all possible entity classes with our annotation
$entityClasses = \array_merge(
$entityClasses,
$this->identifyEntityClasses->__invoke($fileContents),
);
}
}
return $entityClasses;
}
private function findNextFileAndReturnContentsGenerator(string $directory): \Generator
{
// Find the files in the directory
$sourceFinder = new Finder();
$sourceFinder->in($directory)->files()->name('*.php');
// Go through files which were found
foreach ($sourceFinder as $file) {
// @codeCoverageIgnoreStart
// Safety check because Finder can return false if the file was not found
if ($file->getRealPath() === false) {
throw new \InvalidArgumentException('File in source directory not found');
}
// @codeCoverageIgnoreEnd
// Get file contents
$fileContents = \file_get_contents($file->getRealPath());
// @codeCoverageIgnoreStart
// Another safety check because file_get_contents can return false if the file was not found
if ($fileContents === false) {
throw new \InvalidArgumentException('File in source directory could not be retrieved');
}
// @codeCoverageIgnoreEnd
// Return the file contents as a generator
yield $fileContents;
}
}
/**
* @param string|null $overrideConnectionName Connection name should be replaced by this configuration setting
* @param string|null $overrideTableName Table name should be replaced by this configuration setting
* @return string Connection name (as used when decorating the connection) for this entity
*/
private function createRepositoryServicesForEntity(
ContainerBuilder $container,
RepositoryConfigInterface $repositoryConfig,
?string $overrideConnectionName,
?string $overrideTableName,
): string {
// Connection can be overwritten in configuration
$connectionName = $overrideConnectionName ?? $repositoryConfig->getConnectionName();
// No ReadOnly repository - exit early
if (!\class_exists($repositoryConfig->getObjectClass() . 'RepositoryReadOnly')) {
return $connectionName;
}
// Table name can be overwritten in configuration
$tableName = $overrideTableName ?? $repositoryConfig->getTableName();
// Create repository config definition
$repositoryConfigDef = new Definition(RepositoryConfig::class, [
$connectionName,
$tableName,
$repositoryConfig->getTableToObjectFields(),
$repositoryConfig->getObjectToTableFields(),
$repositoryConfig->getObjectClass(),
$repositoryConfig->getObjectTypes(),
$repositoryConfig->getObjectTypesNullable(),
$repositoryConfig->getAutoincrementField(),
]);
// Convention for squirrel connection services
$dbReference = ( \strlen($connectionName) === 0
? DBInterface::class // Default connection
: 'squirrel.connection.' . $connectionName // Specific connection
);
$container->setDefinition(
$repositoryConfig->getObjectClass() . 'RepositoryReadOnly',
new Definition($repositoryConfig->getObjectClass() . 'RepositoryReadOnly', [
new Definition(RepositoryReadOnly::class, [
new Reference($dbReference),
$repositoryConfigDef,
]),
]),
);
// No writeable repository exists
if (!\class_exists($repositoryConfig->getObjectClass() . 'RepositoryWriteable')) {
return $connectionName;
}
$container->setDefinition(
$repositoryConfig->getObjectClass() . 'RepositoryWriteable',
new Definition($repositoryConfig->getObjectClass() . 'RepositoryWriteable', [
new Definition(RepositoryWriteable::class, [
new Reference($dbReference),
$repositoryConfigDef,
]),
]),
);
return $connectionName;
}
private function createTransactionServices(ContainerBuilder $container, array $connectionNames): void
{
foreach ($connectionNames as $connectionName) {
// Default service name conventions
$serviceName = 'squirrel.transaction.' . $connectionName;
$connectionService = 'squirrel.connection.' . $connectionName;
// No connection name means it is the default connection
if (\strlen($connectionName) === 0) {
$serviceName = TransactionInterface::class;
$connectionService = DBInterface::class;
}
$container->setDefinition(
$serviceName,
new Definition(Transaction::class, [new Reference($connectionService)]),
);
}
}
private function createMultiRepositoryServices(ContainerBuilder $container): void
{
// Base multi repository services
$container->setDefinition(
MultiRepositoryReadOnlyInterface::class,
new Definition(MultiRepositoryReadOnly::class),
);
$container->setDefinition(
MultiRepositoryWriteableInterface::class,
new Definition(MultiRepositoryWriteable::class),
);
// Builder multi repository services
$container->setDefinition(
MultiRepositoryBuilderReadOnlyInterface::class,
new Definition(
MultiRepositoryBuilderReadOnly::class,
[new Reference(MultiRepositoryReadOnlyInterface::class)],
),
);
$container->setDefinition(
MultiRepositoryBuilderWriteableInterface::class,
new Definition(
MultiRepositoryBuilderWriteable::class,
[new Reference(MultiRepositoryWriteableInterface::class)],
),
);
}
}