Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add SymfonyBundle [closes #378] #490

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"phpstan/phpstan-nette": "0.12.14",
"phpstan/phpstan-mockery": "0.12.11",
"phpstan/phpstan-strict-rules": "0.12.7",
"symfony/config": "~4.4 || ~5.0",
"symfony/dependency-injection": "~4.4 || ~5.0",
"symfony/http-kernel": "~4.4 || ~5.0",
"nextras/orm-phpstan": "dev-master",
"marc-mabe/php-enum-phpstan": "dev-master",
"tracy/tracy": "~2.3"
Expand Down
30 changes: 30 additions & 0 deletions src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Bridges\SymfonyBundle\DependencyInjection;


use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;


class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('nextras_orm');

$root = $treeBuilder->getRootNode();
assert($root instanceof ArrayNodeDefinition);

// @formatter:off
$root
->children()
->scalarNode('model')
->isRequired()
->end();
// @formatter:on

return $treeBuilder;
}
}
241 changes: 241 additions & 0 deletions src/Bridges/SymfonyBundle/DependencyInjection/NextrasOrmExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Bridges\SymfonyBundle\DependencyInjection;


use Nette\Caching\Cache;
use Nette\Caching\IStorage;
use Nette\Caching\Storages\FileStorage;
use Nette\Utils\Reflection;
use Nextras\Dbal\IConnection;
use Nextras\Orm\Bridges\SymfonyBundle\RepositoryLoader;
use Nextras\Orm\Entity\Reflection\IMetadataParserFactory;
use Nextras\Orm\Entity\Reflection\MetadataParserFactory;
use Nextras\Orm\Exception\InvalidStateException;
use Nextras\Orm\Exception\RuntimeException;
use Nextras\Orm\Mapper\Dbal\DbalMapperCoordinator;
use Nextras\Orm\Model\IModel;
use Nextras\Orm\Model\IRepositoryLoader;
use Nextras\Orm\Model\MetadataStorage;
use Nextras\Orm\Model\Model;
use Nextras\Orm\Repository\IRepository;
use ReflectionClass;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;


class NextrasOrmExtension extends Extension
{
/**
* @param array<mixed> $configs
*/
public function load(array $configs, ContainerBuilder $builder): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$modelClass = $config['model'];
$repositories = $this->findRepositories($builder, $modelClass);
$modelConfig = Model::getConfiguration($repositories);

foreach ($repositories as $repositoryName => $repositoryClass) {
$mapperClass = str_replace('Repository', 'Mapper', $repositoryClass);
$this->setupMapper($builder, $mapperClass);
$this->setupRepository($builder, $repositoryClass, $mapperClass, $modelClass);
}

$this->setupCacheStorage($builder);
$this->setupCache($builder);
$this->setupDbalMapperCoordinator($builder);
$this->setupRepositoryLoader($builder);
$this->setupMetadataParserFactory($builder);
$this->setupMetadataStorage($builder, $modelConfig[2]);
$this->setupModel($builder, $modelClass, $modelConfig);
}


/**
* @return array<string, string>
* @phpstan-param class-string<\Nextras\Orm\Model\IModel> $modelClass
* @phpstan-return array<string, class-string<IRepository<\Nextras\Orm\Entity\IEntity>>>
*/
protected function findRepositories(ContainerBuilder $builder, string $modelClass): array
{
if ($modelClass === Model::class) {
throw new InvalidStateException('Your model has to inherit from ' . Model::class . '. Use compiler extension configuration - model key.');
}

$modelReflection = new ReflectionClass($modelClass);
$classFileName = $modelReflection->getFileName();
assert($classFileName !== false);
$builder->addResource(new FileResource($classFileName));

$repositories = [];
preg_match_all(
'~^ [ \t*]* @property(?:|-read) [ \t]+ ([^\s$]+) [ \t]+ \$ (\w+) ()~mx',
(string) $modelReflection->getDocComment(), $matches, PREG_SET_ORDER
);

/**
* @var string $type
* @var string $name
*/
foreach ($matches as [, $type, $name]) {
/** @phpstan-var class-string<IRepository<\Nextras\Orm\Entity\IEntity>> $type */
$type = Reflection::expandClassName($type, $modelReflection);
if (!class_exists($type)) {
throw new RuntimeException("Repository '{$type}' does not exist.");
}

$rc = new ReflectionClass($type);
assert($rc->implementsInterface(IRepository::class), sprintf(
'Property "%s" of class "%s" with type "%s" does not implement interface %s.',
$modelClass, $name, $type, IRepository::class
));

$repositories[$name] = $type;
}

return $repositories;
}


private function setupCacheStorage(ContainerBuilder $builder): void
{
if ($builder->has(IStorage::class)) {
return;
}

$definition = new Definition(FileStorage::class);
$definition->setArgument('$dir', $builder->getParameter('kernel.cache_dir'));

$builder->setDefinition(FileStorage::class, $definition);
$builder->setAlias(IStorage::class, FileStorage::class);
}


private function setupCache(ContainerBuilder $builder): void
{
if ($builder->has(Cache::class)) {
return;
}

$definition = new Definition(Cache::class);
$definition->setArgument('$storage', new Reference(IStorage::class));
$definition->setArgument('$namespace', 'nextras_orm');

$builder->setDefinition(Cache::class, $definition);
}


private function setupDbalMapperCoordinator(ContainerBuilder $builder): void
{
if ($builder->has(DbalMapperCoordinator::class)) {
return;
}

$definition = new Definition(DbalMapperCoordinator::class);
$definition->setArgument('$connection', new Reference(IConnection::class));

$builder->setDefinition(DbalMapperCoordinator::class, $definition);
}


private function setupMapper(ContainerBuilder $builder, string $mapperClass): void
{
if ($builder->has($mapperClass)) {
return;
}

$definition = new Definition($mapperClass);
$definition->setArgument('$connection', new Reference(IConnection::class));
$definition->setArgument('$mapperCoordinator', new Reference(DbalMapperCoordinator::class));
$definition->setArgument('$cache', new Reference(Cache::class));

$builder->setDefinition($mapperClass, $definition);
}


private function setupRepository(ContainerBuilder $builder, string $repositoryClass, string $mapperClass, string $modelClass): void
{
if ($builder->has($repositoryClass)) {
return;
}

$definition = new Definition($repositoryClass);
$definition->setArgument('$mapper', new Reference($mapperClass));
$definition->addMethodCall('setModel', [new Reference($modelClass)]);
$definition->setPublic(true);

$builder->setDefinition($repositoryClass, $definition);
}


private function setupRepositoryLoader(ContainerBuilder $builder): void
{
if ($builder->has(IRepositoryLoader::class)) {
return;
}

$definition = new Definition(RepositoryLoader::class);
$definition->setArgument('$container', new Reference(ContainerInterface::class));

$builder->setDefinition(RepositoryLoader::class, $definition);
$builder->setAlias(IRepositoryLoader::class, RepositoryLoader::class);
}


private function setupMetadataParserFactory(ContainerBuilder $builder): void
{
if ($builder->has(IMetadataParserFactory::class)) {
return;
}

$definition = new Definition(MetadataParserFactory::class);

$builder->setDefinition(MetadataParserFactory::class, $definition);
$builder->setAlias(IMetadataParserFactory::class, MetadataParserFactory::class);
}


/**
* @param array<string, string> $entityClassMap
*/
private function setupMetadataStorage(ContainerBuilder $builder, array $entityClassMap): void
{
if ($builder->has(MetadataStorage::class)) {
return;
}

$definition = new Definition(MetadataStorage::class);
$definition->setArgument('$entityClassesMap', $entityClassMap);
$definition->setArgument('$cache', new Reference(Cache::class));
$definition->setArgument('$metadataParserFactory', new Reference(IMetadataParserFactory::class));
$definition->setArgument('$repositoryLoader', new Reference(IRepositoryLoader::class));

$builder->setDefinition(MetadataStorage::class, $definition);
}


/**
* @param mixed[] $config
*/
private function setupModel(ContainerBuilder $builder, string $modelClass, array $config): void
{
if ($builder->has($modelClass)) {
return;
}

$definition = new Definition($modelClass);
$definition->setArgument('$configuration', $config);
$definition->setArgument('$repositoryLoader', new Reference(IRepositoryLoader::class));
$definition->setArgument('$metadataStorage', new Reference(MetadataStorage::class));

$builder->setDefinition($modelClass, $definition);
$builder->setAlias(IModel::class, $modelClass);
}
}
11 changes: 11 additions & 0 deletions src/Bridges/SymfonyBundle/NextrasOrmBundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Bridges\SymfonyBundle;


use Symfony\Component\HttpKernel\Bundle\Bundle;


class NextrasOrmBundle extends Bundle
{
}
44 changes: 44 additions & 0 deletions src/Bridges/SymfonyBundle/RepositoryLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Bridges\SymfonyBundle;

use Nextras;
use Nextras\Orm\Repository\IRepository;
use Symfony\Component\DependencyInjection\ContainerInterface;


class RepositoryLoader implements Nextras\Orm\Model\IRepositoryLoader
{
/** @var ContainerInterface */
private $container;


public function __construct(ContainerInterface $container)
{
$this->container = $container;
}


public function hasRepository(string $className): bool
{
return $this->container->has($className);
}


/**
* @phpstan-template R of IRepository<\Nextras\Orm\Entity\IEntity>
* @phpstan-param class-string<R> $className
* @phpstan-return R
*/
public function getRepository(string $className): IRepository
{
/** @phpstan-var R */
return $this->container->get($className);
}


public function isCreated(string $className): bool
{
return $this->container->initialized($className);
}
}