Symfony multiple entity managers with one repository - php

I have a symfony/api-platform application which connects to 2 different databases configured in doctrine.yaml. I want to notice that both databases store the same entities (but with different actions on each), hence we duplicated all the entity classes and created a repository for each, in order for migrations and api-platform actions to work. Since those enities share a lot of common repository functionallity, what I have done so far to remove code
duplication is:
Create entity manager decorators
class AEntityManager extends EntityManagerDecorator{}
class BEntityManager extends EntityManagerDecorator{}
config/services.yaml
App\EntityManager\AEntityManager:
decorates: doctrine.orm.a_entity_manager
App\EntityManager\BEntityManager:
decorates: doctrine.orm.b_entity_manager
App\Repository\Main\AResourceRepository:
arguments:
- '#App\EntityManager\AEntityManager'
App\Repository\ProductPending\BResourceRepository:
arguments:
- '#App\EntityManager\BEntityManager'
Create a base class (for each entity) to share code between the 2 repositories
class RepositoryBase extends ServiceEntityRepository
{
public function __construct(EntityManagerInterface $em, string $class) {...}
// common methods
}
class ARepository extends RepositoryBase
{
public function __construct(EntityManagerInterface $em)
{
parent::__construct($em, A::class);
}
}
class BRepository extends RepositoryBase
{
public function __construct(EntityManagerInterface $em)
{
parent::__construct($em, B::class);
}
}
And entities
/**
#Orm\Entity(repositoryClass=ARepository::class)
*/
class A {
string $prop;
}
/**
#Orm\Entity(repositoryClass=BRepository::class)
*/
class B {
string $prop;
}
Note that all actions for the second database occur in endpoints prefixed with (let's say) /api/b/...
I would like to know if there is a way to eliminate the different repository classes and define the same repository across the 2 different entities. What I have in mind is to change the object which is used to inject the EntityManagerInterface constructor parameter based on the url of the request, but I haven't found something specific about it and I don't know if that's even possible.

Related

Register custom AttributeBag through CompilerPass

I am trying to tidy up my session variables by integrating custom AttributBags into the session. In Symfony < 6.0 you were able to inject a custom AttributBag into the session service.
See related questions
How to add extra bag to symfony session
Using Symfony AttributeBags in a Controller
However this approach does not work anymore in Symfony >= 6.0. This blog article explains that the session service is deprecated and must now be accessed over the request_stack service. For controllers this works fine.
My current (not working) approach looks like this: Define a custom AttributBag class.
class ShoppingCartBag extends AttributeBag {
public function __construct(string $storageKey = 'shoppingCart') {
parent::__construct($storageKey);
}
}
Add a custom CompilerPass in the Kernel class so that Symfony takes care of all changes while building the container.
class Kernel extends BaseKernel {
use MicroKernelTrait;
protected function build(ContainerBuilder $container): void {
$container->addCompilerPass(new AddShoppingCartBagToSessionService());
}
}
The custom CompilerPass looks like this.
class AddShoppingCartBagToSessionService implements CompilerPassInterface {
public function process(ContainerBuilder $container) {
$container->getDefinition('request_stack') //<- Works, but how to access the session?
->addMethodCall('getSession') // How to bridge the gap? This thought does not work. I assume it is because the session is not yet instantiated when the container is build.
->addMethodCall('registerBag', [new Reference('App\Session\CustomBag\ShoppingCartBag')]);
}
}
As you correctly assumed, the session does not exist yet when doing this via the compiler pass.
Symfony uses a so called SessionFactory to create the session. So what you can do instead, is decorating the existing session.factory service with your own implementation of the SessionFactoryInterface and add your attribute bag there:
An implementation of this decorated session factory might look like this:
namespace App;
use Symfony\Component\HttpFoundation\Session\SessionFactoryInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class SessionFactoryWithAttributeBag implements SessionFactoryInterface
{
public function __construct(private SessionFactoryInterface $delegate)
{
}
public function createSession(): SessionInterface
{
$session = $this->delegate->createSession();
$session->registerBag(new ShoppingCartBag());
return $session;
}
}
And then you can decorate the session.factory via the services.yaml:
services:
App\SessionFactoryWithAttributeBag:
decorates: session.factory
arguments: ['#.inner']
Now, whenever a session is created, your custom bag is also registered
That was an important clue, thank you #Spea!
I adopted his idea and created a new decorator for the session service. After some trial and error I found an answer to my problem. The solution looks like this. Notice the actual syntax is slightly different from the answer given by Spea.
Create a custom AttributBag by extending the likewise named class. Be careful to set the name of the attribut bag, not the storage key in constructor. Otherwise Symfony will throw an error when you try to access the ShoppingCartBag.
namepsace App\Session;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
class ShoppingCartBag extends AttributeBag {
public function __construct() {
parent::__construct();
$this->setName('shoppingCart');
}
}
Create a decorator to change the session service's behaviour to get the desired result (include the ShoppingCartBag on each session).
namespace App\Decorator;
use App\Session\ShoppingCartBag;
use Symfony\Component\HttpFoundation\Session\SessionFactoryInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class SessionFactoryShoppingCartBag implements SessionFactoryInterface {
public function __construct(private SessionFactoryInterface $delegate) {}
public function createSession(): SessionInterface {
$session = $this->delegate->createSession();
$session->registerBag(new ShoppingCartBag());
return $session;
}
}
Then decorate the session service in the services.yml by adding the following piece of code.
services:
App\Decorator\SessionFactoryShoppingCartBag:
decorates: session.factory
arguments: ['#.inner']

Symfony 3.3 DI. Access a service name (class name) inside service factory

I want to register all Doctrine repositories as services
I found this approach: How to configure dependency injection for repository class in symfony 3
But this way I have to register every single one repository manually.
I'd like to create some Service Factory that would instantiate repositories for me given just a repository class name.
Something like this:
namespace AppBundle\Service;
use Doctrine\ORM\EntityManagerInterface;
class RepositoryFactory
{
/**
* #var EntityManagerInterface
*/
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function getRepository(string $repositoryClass)
{
// do some magic that somehow obtaining actual repo
return $repo;
}
}
service.yml
AppBundle\Service\RepositoryFactory:
autowire: true
but Symfony's docs say nothing about how to pass service name that you want to instantiate (if it's even possible) in this factory

Subclass Doctrine 2 entity

I am trying to subclass a Doctrine 2 entity to add a bunch of "helper" functions I'd like to use.
For example, this is my entity:
namespace Project\Entity;
class Product
{
private $name;
private $idProductCategory;
}
Mapping is done via XML files.
Then, I try to extend it:
namespace Project\Entity;
class ProductJSON extends Product {
public function toJSON() {
/* ... */
}
}
When I try to use this object in Doctrine:
$a = $entityManager->getRepository('\Project\Entity\ProductJSON');
I get the "No mapping file found named Project.Entity.ProductJSON.dcm.xml" error.
Which is perfectly right, because I do not want any additional mapping.
I've extensively read Doctrine docs and about Mapped Super Classes (Doctrine: extending entity class), but as far as I understand that is for extending Entities in a DB-sense.
I don't care about the database/mapping, I just want to extend entities PHP-wise to use the objects seamlessy in my application.
How to achieve this goal?
You do not need a subclass to add additional methods. Just add the methods to your entity class - Doctrine will just ignore them because there is no mapping information attached to them.
E.g.
class Product
{
private $name;
private $idProductCategory;
/* ... */
public function toJSON() {
/* ... */
}
}

Map subclass as its extended parent

I have created the following abstract class, which use single table inheritance and maps subclasses on the DiscriminatorColumn model.
/**
* #Entity
* #Table(name="entity")
* #InheritanceType("SINGLE_TABLE")
* #DiscriminatorColumn(name="model", type="string")
* #DiscriminatorMap({
* "green" = "model\GreenEntity",
* "blue" = "model\BlueEntity"
* })
*/
abstract class AbstractEntity
{
/** #Id #Column(type="string") */
protected $entity_id;
}
Let's say I extend the abstract class AbstractEntity by some classes:
class GreenEntity extends AbstractEntity {}
class BlueEntity extends AbstractEntity {}
And extend these by some more subclasses
class GreenEntityChildOne extends GreenEntity {}
class GreenEntityChildTwo extends GreenEntity {}
class BlueEntityChildOne extends BlueEntity {}
class BlueEntityChildTwo extends BlueEntity {}
Now, for example, when I instantiate GreenEntityChildOne and persist it to the database, it will throw an exception that I don't have a mapping for it.
What I'm trying to do is get GreenEntityChildOne to be mapped as GreenEntity (or rather, every class which extends a class below AbstractEntity to be mapped as the class which extends the upper abstract class).
Is this at all possible?
It's not possible with pure annotations
Yes, the mapping you are trying to achieve is possible. However, not with pure annotations. The important thing is that Doctrine needs to know all sub classes at runtime. If you do not want to state them explicitly in the annotations of the mapped superclass, you will need to dynamically provide them.
Doctrine event system to the rescue
There is a great blog post on dynamic mapping with Doctrine, which explains how you can use Doctrine event listeners to programmatically change the loaded ClassMetadata.
To dynamically add subclasses to the discriminator map you can implement a Doctrine event listener like the following:
class DynamicDiscriminatorMapSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(Events::loadClassMetadata);
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$metadata = $eventArgs->getClassMetadata();
$metadata->addDiscriminatorMapClass("GreenEntityChildOne", GreenEntityChildOne::class);
}
}
Register your subscriber
Now you only need to register the event subscriber with Doctrine. Ideally, you inject the classes you want to add based on your configuration to the event subscriber.
// create dynamic subscriber based on your config which contains the classes to be mapped
$subscriber = new DynamicDiscriminatorMapSubscriber($config);
$entityManager->getEventManager()->addEventSubscriber($subscriber);
Further reading
Also, have a look at the PHP mapping section in the Doctrine manual and the more informative API docs for the ClassMetadataBuilder.
Answer is possibly on the Doctrine Docs:
"All entity classes that is part of the mapped entity hierarchy (including the topmost class) should be specified in the #DiscriminatorMap"
http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html
You've only specified GreenEntity and BlueEntity.
I don't know what I'm talking about. This is the first thing I've ever read about Doctrine...

Symfony2 global functions

For example i have algorithmic function, which calculates specific hash-code. Function itself is 300+ lines of code. I need to use that functions many times in many different controllers in my bundle. Where can i store my calculate_hash() to use it in my bundle ? Can i access it from other bundles ?
Can i also write global calculate_hash() which have access to entity manager ?
Didn't find my answer here.
In the Symfony2 world, this is clearly belonging to a service. Services are in fact normal classes that are tied to the dependency injection container. You can inject them the dependencies you need. For example, say your class where the function calculate_hash is located is AlgorithmicHelper. The service holds "global" functions. You define your class something like this:
namespace Acme\AcmeBundle\Helper;
// Correct use statements here ...
class AlgorithmicHelper {
private $entityManager;
public function __construct(EntityManager $entityManager) {
$this->entityManager = $entityManager;
}
public function calculate_hash() {
// Do what you need, $this->entityManager holds a reference to your entity manager
}
}
This class then needs to be made aware to symfony dependecy container. For this, you define you service in the app/config/config.yml files by adding a service section like this:
services:
acme.helper.algorithmic:
class: Acme\AcmeBundle\Helper\AlgorithmicHelper
arguments:
entityManager: "#doctrine.orm.entity_manager"
Just below the service, is the service id. It is used to retrieve your service in the controllers for example. After, you specify the class of the service and then, the arguments to pass to the constructor of the class. The # notation means pass a reference to the service with id doctrine.orm.entity_manager.
Then, in your controller, you do something like this to retrieve the service and used it:
$helper = $this->get('acme.helper.algorithmic');
$helper-> calculate_hash();
Note that the result of the call to $this->get('acme.helper.algorithmic') will always return the same instance of the helper. This means that, by default, service are unique. It is like having a singleton class.
For further details, I invite you to read the Symfony2 book. Check those links also
The service container section from Symfony2 book.
An answer I gave on accesing service outside controllers, here.
Hope it helps.
Regards,
Matt
Braian in comment asked for Symfony 3 answer, so here is one Symfony 3.3 (released May 2017):
1. The original class remains the same
namespace Acme\AcmeBundle\Helper;
use Doctrine\ORM\EntityManager;
final class AlgorithmicHelper
{
/**
* #var EntityManager
*/
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function calculateHash()
{
// Do what you need, $this->entityManager holds a reference to your entity manager
}
}
2. Service registration is much simpler
# app/config/services.yml
services:
_defaults: autowire # this enabled constructor autowiring for all registered services
Acme\AcmeBundle\Helper\AlgorithmicHelper: ~
3. Use constructor injection to get the service
use Acme\AcmeBundle\Helper\AlgorithmicHelper;
class SomeController
{
/**
* #var AlgorithmicHelper
*/
private $algorithmicHelper;
public function __construct(AlgorithmicHelper $algorithmicHelper)
{
$this->algorithmicHelper = $algorithmicHelper;
}
public function someAction()
{
// some code
$hash = $this->algorithmicHelper->calculateHash();
// some code
}
}
You can read about Symfony 3.3 dependency injection (in this case registering services in config and using it in controller) news in these 2 posts:
https://www.tomasvotruba.cz/blog/2017/05/07/how-to-refactor-to-new-dependency-injection-features-in-symfony-3-3/
https://symfony.com/blog/the-new-symfony-3-3-service-configuration-changes-explained

Categories