How should one implement a custom Doctrine purger when using Symfony 6? - php

The Symfony docs shows a solution, but it doesn't appear to work (i.e. Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory needs to be replaced with Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory, and other changes). I modified the code as shown below, but am pretty certain I am not doing it correctly.
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurgerFactory implements PurgerFactory
{
public function __construct(private ORMPurgerFactory $purgeFactory)
{
}
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
// Change $excluded, $purgeWithTruncate as desired.
return new CustomPurger($emName, $em, $excluded, $purgeWithTruncate, $this->purgeFactory);
}
}
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurger implements ORMPurgerInterface
{
public function __construct(private ?string $emName, private EntityManagerInterface $entityManager, private array $excluded, private bool $purgeWithTruncate, private ORMPurgerFactory $purgeFactory)
{
}
public function setEntityManager(EntityManagerInterface $entityManager):void
{
// Seems rather redundent doing this even though I earlier inject $entityManager.
$this->entityManager = $entityManager;
}
public function purge() : void
{
// Delete any tables which must be deleted first to prevent FK constraint errors.
// This doesn't seem write.
$purger = $this->purgeFactory->createForEntityManager($this->emName, $this->entityManager, $this->excluded, $this->purgeWithTruncate);
$purger->purge();
}
}
services:
App\DataFixtures\Purger\DoctrinePurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
arguments:
- '#doctrine.fixtures.purger.orm_purger_factory'
Or should it be done by decorating the default purger as suggested by this post?

Okay. So you do have a few things wrong and the docs are somewhat out of date. From a big picture point of view you want something like:
bin/console doctrine:fixtures:load --purger=my_purger
to use your custom purger factory (aliased as my_purger) to instantiate and execute your custom purger's purge method. The job of the factory is to just create the purger not to execute it.
I followed the docs and implemented PurgerInterface but the purge command complained about it not implementing ORMPurgerInterface which, as you noted, adds a seemingly superfluous method. I think it is still a work in progress. The default ORMPurger has a couple of additional public methods not defined in any interface which is also strange. The fact that Doctrine is inconsistent with it's usage of the Interface suffix does not help. But it is what it is.
This works under 6.1:
# CustomPurger.php
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurger implements ORMPurgerInterface
{
private EntityManagerInterface $em;
public function setEntityManager(EntityManagerInterface $em) : void
{
$this->em = $em;
}
public function purge() : void
{
dd(' my purger');
}
}
# CustomPurgerFactory.php
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurgerFactory implements PurgerFactory
{
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
return new CustomPurger($em);
}
}
# services.yaml
App\Purger\CustomPurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
bin/console doctrine:fixtures:load --purger=my_purger
> purging database
^ " my purger"
As far as decorating goes, you decorate a service when you want to modify some methods without extending the original class. There is only one method here and it's quite a doozy so I don't think decorating will help.
If you wanted to always use your purger without the --purger option then you could probably point the default purger factory service id to your factory. I'll leave that as an exercise for the reader.
One final note: I took a look at your decorating link. Don't know what they were trying to do but I do know it has nothing to do with decorating.

Related

New alternative for getDoctrine() in Symfony 5.4 and up

As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.

How can I get a collection of all monolog loggers in Symfony?

I have a factory class that creates different objects and I have a lot of loggers defined for each type of object.
And I want to get all loggers collection in my factory. Before I just used a ContainerInterface in my factory constructor, but since Symfony 5.1 container autowiring is deprecated.
Now I can not find a way to get a collection of loggers. I tried to use
!tagged_iterator { tag: 'monolog.logger' }
and also tried to set a tag for LoggerInterface and get a tagged_iterator for it, but it didn't work. I suggest that it is because loggers are not real classes.
This might be overkill but as you say the logger services are a bit unusual. Seems like they should be tagged or be taggable by LoggerInterface but I ran some test and could not get it to work. Here is a brute force approach which relies on logger services having ids of monolog.logger.name:
# Start with a service locator class
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
class LoggerLocator extends ServiceLocator
{
// Just to specify the return type to keep IDEs happy
public function get($id) : LoggerInterface
{
return parent::get($id);
}
}
...
# now make the kernel into a compiler pass
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
...
public function process(ContainerBuilder $container)
{
$loggerServices = [];
foreach($container->getServiceIds() as $id) {
if (!strncmp($id,'monolog.logger.',15)) {
//echo 'Logger ' . $id . "\n";
$loggerServices[$id] = new Reference($id);
}
}
$loggerLocator = $container->getDefinition(LoggerLocator::class);
$loggerLocator->setArguments([$loggerServices]);
}
# and now we can inject the locator where it is needed
class SomeController {
public function index(Request $request, LoggerLocator $loggerLocator)
{
dump($loggerLocator);
$logger = $loggerLocator->get('monolog.logger.' . $name);
Seems like there should be a way to do this just through configuring but a pass is easy enough I guess.

PHPUnit: How to test Doctrine Repository method that returns void?

I have a Doctrine repository class that can be used to persist a User, I don't want to check if the entity was really persisted or not, I just want to know what to do in this situation:
It's easy to test a repository method that has a return value. But in this case, I have nothing to do, and I'd like to cover 100% of the code, without making unsafe code that can break like use #addToAssertionCount.
<?php
namespace Domain\Repository;
use DateTime;
use Domain\Entity\User;
use Domain\Repository\Interfaces\UserRepositoryInterface;
class UserRepository extends Repository implements UserRepositoryInterface
{
public function create(User $user): void
{
$user->setCreatedAt(new DateTime('now'));
$this->getEntityManager()->persist($user);
}
}
And a testing class for it:
<?php
namespace Domain\Repository;
use Doctrine\ORM\EntityManager;
use Domain\Entity\User;
use PHPUnit\Framework\TestCase;
class UserRepositoryTest extends TestCase
{
private UserRepository $sut;
public function setUp(): void
{
$entity_manager = $this->createMock(EntityManager::class);
$this->sut = new UserRepository($entity_manager);
}
public function test_assert_create(): void
{
$user = $this->createMock(User::class);
$this->sut->create($user);
// What to assert??
}
}
At this point, I don't know even what to assert, once the persist() method returns void, which I can't mock.
Focusing on 100% code coverage is not a good idea and encourages writing tests that have little to no value. What does that mean? The create method has two side effects: It changes the users creation date and persists it. You could test it like this:
final class UserRepositoryTest extends TestCase
{
/**
* #var EntityManager&MockObject
*/
private EntityManager $entityManager;
private UserRepository $sut;
public function setUp(): void
{
$this->entityManager = $this->createMock(EntityManager::class);
$this->sut = new UserRepository($this->entityManager);
}
public function test_create_should_persist_entity(): void
{
$user = new User();
$user->setCreatedAt(new DateTime('2000-01-01 12:15:30'));
// validate that persist call was made
$this->entityManager->expects(self::once())
->method('persist')
->with($user);
$this->sut->create($user);
// validate that creation date was set
self::assertEqualsWithDelta(new DateTime('now'), $user->getCreatedAt(), 3);
}
}
You could go even one step further and verify that the creation date was set before the persist call was made by using the callback constraint. But then, you're pretty much checking the implementation line by line in your test. That's how people end with tests that break all the time.
So, what to do instead? Focus on the purpose of the user repository: If you put something in, you should be able to get it out. This, however, requires that you use an actual entity manager. But you wrote, that you don't want to check that the entity was actually persisted. In that case, I would rather write no tests than the example I gave above.

How to add an entity-specific listener in Symfony2 that has access to container

In a Symfony2 application, I have an entity that needs to be populated on pre-persist with various context properties (like user id, what page it was called from, etc.)
I figured that to do this, I need to add a doctrine event listener that has access to "service_container", and the best way to give such access is to pass "service_container" as an argument to this listener.
I have a specific entity that I want to listen to, and I do not want to trigger the listener to events with any other entity.
We can add an entity-specific listener, documentation is found here:
http://docs.doctrine-project.org/en/latest/reference/events.html#entity-listeners
- but this does not provide example of how to pass an argument (I use PHP annotations to declare the listener).
I also tried to use JMSDiExtraBundle annotations, like in the example below:
http://jmsyst.com/bundles/JMSDiExtraBundle/master/annotations#doctrinelistener-or-doctrinemongodblistener
- but this way requires to declare the listener as non-entity-specific
Is there any way to make a listener for one entity only, and have it have access to container?
One of the ways similar to doctrine docs through dependency injection:
<?php
namespace AppBundle\EntityListener;
use AppBundle\Entity\User;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\RouterInterface;
class UserListener {
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function postPersist(User $user, LifecycleEventArgs $args)
{
$logger = $this->logger;
$logger->info('Event triggered');
//Do something
}
}
services:
user.listener:
class: AppBundle\EntityListener\UserListener
arguments: [#logger]
tags:
- { name: doctrine.orm.entity_listener }
And dont forget add listener to entity mapping:
AppBundle\Entity\User:
type: entity
table: null
repositoryClass: AppBundle\Entity\UserRepository
entityListeners:
AppBundle\EntityListener\UserListener: ~
I would simply check entity type from the event. If you check type inside or outside the subscriber, it has the same performance cost. And simple type condition is fast enough.
namespace App\Modules\CoreModule\EventSubscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
class SetCountryToTaxSubscriber implements EventSubscriber
{
/**
* {#inheritdoc}
*/
public function getSubscribedEvents()
{
return [Events::prePersist];
}
public function prePersist(LifecycleEventArgs $lifecycleEventArgs)
{
$entity = $lifecycleEventArgs->getEntity();
if ( ! $entity instanceof Tax) {
return;
}
$entity->setCountry('myCountry');
}
}

Symfony 2.0 getting service inside entity

Im seraching over and cannot find answer.
I have database role model in my application. User can have a role but this role must be stored into database.
But then user needs to have default role added from database. So i created a service:
<?php
namespace Alef\UserBundle\Service;
use Alef\UserBundle\Entity\Role;
/**
* Description of RoleService
*
* #author oracle
*/
class RoleService {
const ENTITY_NAME = 'AlefUserBundle:Role';
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function findAll()
{
return $this->em->getRepository(self::ENTITY_NAME)->findAll();
}
public function create(User $user)
{
// possibly validation here
$this->em->persist($user);
$this->em->flush($user);
}
public function addRole($name, $role) {
if (($newrole = findRoleByRole($role)) != null)
return $newrole;
if (($newrole = findRoleByName($name)) != null)
return $newrole;
//there is no existing role
$newrole = new Role();
$newrole->setName($name);
$newrole->setRole($role);
$em->persist($newrole);
$em->flush();
return $newrole;
}
public function getRoleByName($name) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('name' => $name));
}
public function getRoleByRole($role) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('role' => $role));
}
}
my services.yml is:
alef.role_service:
class: Alef\UserBundle\Service\RoleService
arguments: [%doctrine.orm.entity_manager%]
And now I want to use it in two places:
UserController and User entity. How can i get them inside entity?
As for controller i think i just need to:
$this->get('alef.role_service');
But how to get service inside entity?
You don't. This is a very common question. Entities should only know about other entities and not about the entity manager or other high level services. It can be a bit of a challenge to make the transition to this way of developing but it's usually worth it.
What you want to do is to load the role when you load the user. Typically you will end up with a UserProvider which does this sort of thing. Have you read through the sections on security? That should be your starting point:
http://symfony.com/doc/current/book/security.html
The reason why it's so difficult to get services into entities in the first place is that Symfony was explicitly designed with the intent that services should never be used inside entities. Therefore, the best practice answer is to redesign your application to not need to use services in entities.
However, I have found there is a way to do it that does not involve messing with the global kernel.
Doctrine entities have lifeCycle events which you can hook an event listener to, see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events For the sake of the example, I'll use postLoad, which triggers soon after the Entity is created.
EventListeners can be made as services which you inject other services into.
Add to app/config/config.yml:
services:
example.listener:
class: Alef\UserBundle\EventListener\ExampleListener
arguments:
- '#alef.role_service'
tags:
- { name: doctrine.event_listener, event: postLoad }
Add to your Entity:
use Alef\UserBundle\Service\RoleService;
private $roleService;
public function setRoleService(RoleService $roleService) {
$this->roleService = $roleService;
}
And add the new EventListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Service\RoleService;
class ExampleListener
{
private $roleService;
public function __construct(RoleService $roleService) {
$this->roleService = $roleService;
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(method_exists($entity, 'setRoleService')) {
$entity->setRoleService($this->roleService);
}
}
}
Just keep in mind this solution comes with the caveat that this is still the quick and dirty way, and really you should consider redesigning your application the proper way.
Thanks to Kai's answer above which answer to the question, but it's not compatible with symfony 5.x .
It's good to precise it's a bad practice, but required in some special case like legacy code or a bad DB design (as a temporary solution before schema migration)
As in my case, I use this code with a mailer and translator, which introduce an issue with the private property if Symfony >= 5.3 , so here the solution for recent version of symfony:
in config/services.yaml:
services:
Alef\UserBundle\EventListener\ExampleListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
ExampleListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Entity\Role;
class ExampleListener
{
public function postLoad(LifecycleEventArgs $postLoad): void
{
$entity = $postLoad->getEntity();
if ($entity instanceof User) {
$repository = ;
$entity->roleRepository(
$postLoad->getEntityManager()->getRepository(Role::class)
);
}
}
}
And in your Entity (or in a trait if you use it in more than one entity):
use Alef\UserBundle\Service\RoleService;
/** #internal bridge for legacy schema */
public function roleRepository(?RoleRepository $repository = null) {
static $roleRepository;
if (null !== $repository) {
$roleRepository = $repository;
}
return $roleRepository;
}
public function getRoleByName($name) {
return $this->roleRepository()->findBy(array('name' => $name));
}

Categories