Factory creation with injected entity repository
namespace App\DependencyInjection\Compiler;
use App\Factory\EntityFactoryRegistry;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class EntityFactoryPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has(EntityFactoryRegistry::class)) {
return;
}
$definition = $container->findDefinition(EntityFactoryRegistry::class);
$taggedServices = $container->findTaggedServiceIds('app.entity_factory');
$entityManager = $container->findDefinition('doctrine.orm.entity_manager');
foreach ($taggedServices as $id => $tags) {
$definition->addMethodCall('addFactory', [new Reference($id), $entityManager]);
}
}
}
//using code like this to get factory for entity, now this part works, I have the factory accessible everywhere I want.
foreach ((new \ReflectionClass($this::class))->getAttributes(Access::class) as $attribute) {
if (key_exists('entity', $attribute->getArguments())) {
$this->factory = $this->entityFactoryRegistry->get($attribute->getArguments()['entity']);
}
}
Post factory:
<?php
namespace App\Factory;
use App\Command\EntityCommandInterface;
use App\Command\PostCommand;
use App\Entity\Base\BaseEntity;
use App\Entity\Post;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(tags: ['app.entity_factory'])]
class PostFactory implements EntityFactoryInterface
{
private ?EntityManagerInterface $entityManager = null;
public function create(EntityCommandInterface|PostCommand $command): BaseEntity
{
}
public function edit(BaseEntity|Post $entity, EntityCommandInterface|PostCommand $command): BaseEntity
{
}
public function getMainPosts (): array
{
return $this->entityManager->getRepository($this->getEntity())
->findAll();
}
public function setEntityManager (EntityManagerInterface $entityManager): void
{
$this->entityManager = $entityManager;
}
public function getEntity(): string
{
return Post::class;
}
}
now when it comes to twig extension, I would like to get factory the same way and pass it to custom twig node.
Extension creation:
namespace App\Extension;
use App\Extension\Parser\FactoryTokenParser;
use App\Factory\EntityFactoryRegistry;
use Twig\Extension\AbstractExtension;
class FactoryExtension extends AbstractExtension
{
public function __construct(
protected readonly EntityFactoryRegistry $entityFactoryRegistry,
)
{
}
public function getTokenParsers(): array
{
return [
new FactoryTokenParser($this->entityFactoryRegistry),
];
}
}
Token parser:
<?php
namespace App\Extension\Parser;
use App\Extension\Node\FactoryTagNode;
use App\Factory\EntityFactoryRegistry;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
class FactoryTokenParser extends AbstractTokenParser
{
public function __construct(
protected readonly EntityFactoryRegistry $entityFactoryRegistry,
)
{}
public function parse(Token $token): FactoryTagNode
{
$stream = $this->parser->getStream();
$entityClass = $this->parser->getExpressionParser()->parseExpression();
if (!class_exists($entityClass->getAttribute("value"))) {
throw new \Exception("Class provided to factory does not exist");
}
// Parse the content between {% factory %} and {% endfactory %}
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideFactoryEndTag'], true);
$stream->expect(Token::BLOCK_END_TYPE);
$entityFactory = $this->entityFactoryRegistry->get($entityClass->getAttribute("value"));
return new FactoryTagNode(
$entityFactory,
$body,
$token->getLine(),
$this->getTag()
);
}
public function decideFactoryEndTag(Token $token): bool
{
return $token->test('endfactory');
}
public function getTag(): string
{
return 'factory';
}
}
Node:
<?php
namespace App\Extension\Node;
use App\Factory\EntityFactoryInterface;
use Twig\Compiler;
use Twig\Node\Node;
class FactoryTagNode extends Node
{
private EntityFactoryInterface $factory;
public function __construct(EntityFactoryInterface $entityFactory, Node $body, int $line, string $tag)
{
parent::__construct(['body' => $body], [], $line, $tag);
$this->factory = $entityFactory;
}
public function compile(Compiler $compiler): void
{
$body = $this->getNode('body');
$compiler
->addDebugInfo($this)
->write(sprintf('$context["factory"] = %s;', $this->getVarExport($this->factory)))
->write('ob_start();' . PHP_EOL)
->subcompile($body)
->write('echo strtoupper(ob_get_clean());' . PHP_EOL);
}
private function getVarExport($var): ?string
{
return var_export($var, true);
}
}
Now this is the part, where it wont work.
->write(sprintf('$context["factory"] = %s;', $this->getVarExport($this->factory)))
Error: An exception has been thrown during the compilation of a template ("Warning: var_export does not handle circular references").
When I initialize class like this
->write(sprintf('$context["factory"] = new %s();', get_class(this->factory)))
Twig has the class available, but I want to inject the factory class, not to re-init new instance of a class for twig.
Why, reason is that, entity manager is injected into factories using compiler pass and when I re-init class, it is no longer available.
Are there more solutions to this I am unable to figure out?
Twig rendering part:
{% factory 'App\\Entity\\Post' %}
{{ dump(factory.mainPosts) }}
{% endfactory %}
Okay, got it figured out, basically what I did
FactoryExtension.php:
<?php
namespace App\Extension;
use App\Extension\Parser\FactoryTokenParser;
use App\Factory\EntityFactoryRegistry;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class FactoryExtension extends AbstractExtension
{
public function __construct(
protected readonly EntityFactoryRegistry $entityFactoryRegistry
)
{
}
public function getTokenParsers(): array
{
return [
new FactoryTokenParser(),
];
}
public function getEntityFactory(string $className): ?object
{
return $this->entityFactoryRegistry->get($className);
}
}
Main thing: FactoryTagNode.php:
<?php
namespace App\Extension\Node;
use Twig\Compiler;
use Twig\Node\Node;
class FactoryTagNode extends Node
{
public function __construct(Node $entityClass, Node $body, int $line, string $tag)
{
parent::__construct(['entityClass' => $entityClass, 'body' => $body], [], $line, $tag);
}
public function compile(Compiler $compiler): void
{
$body = $this->getNode('body');
$entityClass = $this->getNode('entityClass');
$compiler
->addDebugInfo($this)
->write(sprintf('$context["factory"] = $this->env->getExtension(\'App\\Extension\\FactoryExtension\')->getEntityFactory(\'%s\');' . PHP_EOL, $entityClass->getAttribute("value")))
->write('ob_start();' . PHP_EOL)
->subcompile($body)
->write('echo strtoupper(ob_get_clean());' . PHP_EOL);
}
}
And twig:
{% factory 'App\\Entity\\Post' %}
{{ dump(factory.mainPosts|length) }} {# prints out 2 in my case #}
{% endfactory %}