I am using RabbitMQBundle in a Symfony 2.8 project, and I would like to use a custom producer class which persists an entity (Message) in database before publishing the RabbitMQ message.
I defined the custom producer class in config.yml:
old_sound_rabbit_mq:
...
producers:
myproducer:
class: AppBundle\Services\GenericProducer
connection: default
exchange_options: {name: 'my_exchange', type: direct}
And the custom Producer class:
<?php
namespace AppBundle\Services;
use AppBundle\Entity\Message;
use OldSound\RabbitMqBundle\RabbitMq\Producer;
/**
* Customised Producer, that publishes AMQP Messages
* but also:
* - writes an entry in the Message table
*/
class GenericProducer extends Producer
{
/**
* Entity Manager
*/
protected $em;
public function setEntityManager($entityManager)
{
$this->em = $entityManager;
return $this;
}
/**
* Publishes the message and merges additional properties with basic properties
* And also:
* - writes an entry in the Message table
*
* #param string $action
* #param array $parameters
* #param string $routingKey
* #param array $additionalProperties
* #param array|null $headers
*/
public function publish($action, $parameters = array() , $routingKey = '', $additionalProperties = array(), array $headers = null)
{
$message = new Message();
$message->setAction($action)
->setParameters($parameters);
$this->em->persist($message);
$this->em->flush();
$msgBody = array(
'action' => $action,
'parameters' => $parameters
);
parent::publish($msgBody, $routingKey, $additionalProperties, $headers);
}
}
How can I make a call to GenericProducer->setEntityManager, as the producer is not defined in services.yml, like other services ?
Is there another way to achieve this ?
Thanks for your time.
the producer service definition is generated dynamically by the bundle in the Dependency Injection Extension of the bundle.
You can either try to decorate the existing service or create a compiler pass where you fetch the existing service and extend it by calling the setEntityManager function.
Following #lordrhodos suggestion, I decorated the producer service generated by RabbitMQBundle. Here is the complete code:
config.yml (nothing special to do):
old_sound_rabbit_mq:
...
producers:
myproducer:
connection: default
exchange_options: {name: 'my_exchange', type: direct}
services.yml (here you define a decorating service):
app.decorating_myproducer_producer:
class: AppBundle\Services\GenericProducer
decorates: old_sound_rabbit_mq.myproducer_producer
arguments: ['#
app.decorating_myproducer_producer.inner', '#doctrine.orm.entity_manager']
public: false
decorator class:
<?php
namespace AppBundle\Services;
use AppBundle\Entity\Message;
use OldSound\RabbitMqBundle\RabbitMq\Producer;
/**
* Customised Producer, that publishes AMQP Messages
* but also:
* - writes an entry in the Message table
*/
class GenericProducer extends Producer
{
/**
* #var Producer
*/
protected $producer;
/**
* #var EntityManager
*/
protected $em;
/**
* GenericProducer constructor.
* #param Producer $producer
* #param EntityManager $entityManager
*/
public function __construct(Producer $producer, EntityManager $entityManager)
{
$this->producer = $producer;
$this->em = $entityManager;
}
/**
* Publishes the message and merges additional properties with basic properties
* And also:
* - writes an entry in the Message table
*
* #param string $action
* #param array $parameters
* #param string $routingKey
* #param array $additionalProperties
* #param array|null $headers
*/
public function publish($action, $parameters = array() , $routingKey = '', $additionalProperties = array(), array $headers = null)
{
$message = new Message();
$message->setAction($action)
->setParameters($parameters);
$this->em->persist($message);
$this->em->flush();
$msgBody = array(
'action' => $action,
'parameters' => $parameters
);
$this->producer->publish(serialize($msgBody), $routingKey, $additionalProperties, $headers);
}
}
Finally, call the original producer from a controller:
$this->get('old_sound_rabbit_mq.myproducer_producer')->publish('wait', ['time' => 30]);
Related
I have the structure like below.
----------------
MESSAGE
----------------
id
subject
body
----------------
----------------
USER
----------------
id
name
category
region
----------------
----------------
RECIPIENT
----------------
user_id
message_id
is_read
read_at
----------------
So Message 1:n Recipient m:1 User.
Recipient is not an #ApiResource.
A Backoffice user will "write" a message and choose the audience by a set of specific criteria (user region, user category, user tags...).
To POST the message i'm using a Dto
class MessageInputDto
{
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $subject;
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $body;
/**
* #var bool
*
* #Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* #var DateTimeInterface
*
* #Groups({"msg_message:write"})
*/
public DateTimeInterface $publishDate;
/**
* #var DateTimeInterface|null
*
* #Groups({"msg_message:write"})
*/
public ?DateTimeInterface $expiryDate = null;
/**
* #var MessageCategory|null
*
* #Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* #var array
*/
public array $criteria = [];
}
The $criteria field is used to choose the audience of that message and is skipped by the DataTransformer as it is not a mapped field, a property of Message Entity that is returned by the transformer.
class MessageInputDataTransformer implements \ApiPlatform\Core\DataTransformer\DataTransformerInterface
{
/**
* #var MessageInputDto $object
* #inheritDoc
*/
public function transform($object, string $to, array $context = [])
{
$message = new Message($object->subject, $object->body);
$message->setIsPublished($object->isPublished);
$message->setPublishDate($object->publishDate);
$message->setExpiryDate($object->expiryDate);
$message->setCategory($object->category);
return $message;
}
/**
* #inheritDoc
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
// if it's a book we transformed the data already
if ($data instanceof Message) {
return false;
}
return Message::class === $to && null !== ($context['input']['class'] ?? null);
}
}
As side effect, will be performed a bulk insert in the join table (Recipient) that keeps the m:n relations between Message and User.
My problem is how/where to perform this bulk insert and how pass the $criteria to the service that will manage it.
The only solution that i've found now (and it's working but i don't think is a good practice) is to put the bulk insert procedure in the POST_WRITE event of the Message, get the Request object and process the $criteria contained there.
class MessageSubscriber implements EventSubscriberInterface
{
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
['handleCriteria', EventPriorities::POST_WRITE]
],
];
}
public function handleCriteria(ViewEvent $event)
{
/** #var Message $message */
$message = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$e = $event->getRequest();
$collectionOperation = $e->get('_api_collection_operation_name');
if (!$message instanceof Message ||
$method !== Request::METHOD_POST ||
$collectionOperation !== 'post') {
return;
}
$content = json_decode($event->getRequest()->getContent(), true);
if(array_key_exists('audienceCriteria', $content)){
$criteria = Criteria::createFromArray($content['audienceCriteria']);
// Todo: Create the audience
}
}
}
So the idea is that, when the Message is persisted, the system must generate the "relations" public.
This is why i think that the post write event could be a good choice, but as i said i'm not sure this could be a good practice.
Any idea? Thanks.
As the docs on DTO's state: "in most cases the DTO pattern should be implemented using an API Resource class representing the public data model exposed through the API and a custom data provider. In such cases, the class marked with #ApiResource will act as a DTO."
IOW specifying an Input or an Output Data Representation and a DataTransformer is the exception. It does not work if the DTO holds more data then the entity or if the dto's are not one to one with the entities (for example with a report that does a group by).
Here is your DTO class as a resource:
namespace App\DTO;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use App\Entity\Message;
/**
* Class defining Message data transfer
*
* #ApiResource(
* denormalizationContext= {"groups" = {"msg_message:write"}},
* itemOperations={
* },
* collectionOperations={
* "post"={
* "path"="/messages",
* "openapi_context" = {
* "summary" = "Creates a Message",
* "description" = "Creates a Message"
* }
* }
* },
* output=Message::class
* )
*/
class MessageInputDto
{
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $subject;
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $body;
/**
* #var bool
*
* #Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* #var \DateTimeInterface
*
* #Groups({"msg_message:write"})
*/
public \DateTimeInterface $publishDate;
/**
* #var \DateTimeInterface|null
*
* #Groups({"msg_message:write"})
*/
public ?\DateTimeInterface $expiryDate = null;
/**
* #var MessageCategory|null
*
* #Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* #var array
* #Groups({"msg_message:write"})
*/
public array $criteria = [];
}
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
If MessageInputDto is in /src/DTO make it like:
api_platform:
mapping:
paths:
- '%kernel.project_dir%/src/Entity'
- '%kernel.project_dir%/src/DTO'
The post operation may have the same path as dhe default post operation on your Message resource. Remove that by explicitly defining collectionOperations for your Message resource without "post".
The post operation of MessageInputDto will deserialize the MessageInputDto. Your DataTransformer will not act on it so that it will arrive as is to the DataPersister:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\MessageInputDto;
use App\Entity\Message;
use Doctrine\Persistence\ManagerRegistry;
use App\DataTransformer\MessageInputDataTransformer;
use ApiPlatform\Core\Exception\InvalidArgumentException;
class MessageDataPersister implements ContextAwareDataPersisterInterface
{
private $dataPersister;
private $entityManager;
private $dataTransformer;
public function __construct(ContextAwareDataPersisterInterface $dataPersister, ManagerRegistry $managerRegistry, MessageInputDataTransformer $dataTransformer)
{
$this->dataPersister = $dataPersister;
$this->entityManager = $managerRegistry->getManagerForClass(Message::class);
$this->dataTransformer = $dataTransformer;
}
public function supports($data, array $context = []): bool
{
$transformationContext = ['input' => ['class' => Message::class]];
return get_class($data) == MessageInputDto::class
&& $this->dataTransformer->supportsTransformation($data, Message::class, $transformationContext)
&& null !== $this->entityManager;
}
public function persist($data, array $context = [])
{
$message = $this->dataTransformer->transform($data, Message::class);
// dataPersister will flush the entityManager but we do not want incomplete data inserted
$this->entityManager->beginTransaction();
$commit = true;
$result = $this->dataPersister->persist($message, []);
if(!empty($data->criteria)){
$criteria = Criteria::createFromArray($data->criteria);
try {
// Todo: Create the audience, preferably with a single INSERT query SELECTing FROM user_table WHERE meeting the criteria
// (Or maybe better postpone until message is really sent, user region, category, tags may change over time)
} catch (\Exception $e) {
$commit = false;
$this->entityManager->rollback();
}
}
if ($commit) {
$this->entityManager->commit();
}
return $result;
}
public function remove($data, array $context = [])
{
throw new InvalidArgumentException('Operation not supported: delete');
}
}
(Maybe it should have been called MessageInputDtoDataPersister - depending on how you look at it)
Even with service autowiring and autoconfiguration enabled, you must still configure it to get the right dataPersister to delegate to:
# api/config/services.yaml
services:
# ...
'App\DataPersister\MessageDataPersister':
arguments:
$dataPersister: '#api_platform.doctrine.orm.data_persister'
This way you do not need MessageSubscriber.
Be aware that all the other phases inbetween deserialization and data persist (validation, security post denormalize) work on the MessageInputDto.
One solution when you have to generate multiple custom entities is to use data persisters: https://api-platform.com/docs/core/data-persisters/
There you have 2 options:
Decorate the doctrine persister - meaning the message will still be saved by Doctrine, but you can do something before or afterwards.
Implement a custom persister - saving both message and other related entities that you like. Or doing something completely custom, without calling Doctrine at all.
I've built an api using JMS serialize bundle. Moreover, I have an entity Ticket, which has a repository called TicketRepository. This repository contains the following method:
/**
* #param Ticket $object
*
* #return string
*/
public function findByTicketDeadline($object)
{
// some logic
}
My goal: Pass The returned value of this function to json using EventSubscriber.
Here is my class TicketSerializationSubscriber
class TicketSerializationSubscriber implements EventSubscriberInterface
{
/**
* #param ObjectEvent $event
*/
public function onPostSerialize(ObjectEvent $event)
{
/** #var JsonSerializationVisitor $visitor */
$visitor = $event->getVisitor();
$visitor->setData('ticketDeadline', 'TEST TICKET'); // WORKS
/** #var Ticket $ticket */
$ticket = $event->getObject();
/** #var TicketRepository $ticketRepo */
$ticketRepo = $event->getObject();
$visitor->setData('TESTING', $ticketRepo->findByTicketDeadline($ticket));
// $visitor->setData('uri',
// $this->router->generate('ticket_api_show', [
// 'ticketDeadline' => $ticketRepo->findByTicketDeadline($ticket)
// ])
// );
}
public static function getSubscribedEvents()
{
return array(
array(
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
'class' => 'CMS3\CoreBundle\Entity\Ticket', // if no class, subscribe to every serialization
//'class' => 'CMS3\CoreBundle\Repository\TicketRepository', // if no class, subscribe to every serialization
'format' => 'json', // optional format
//'priority' => 0, // optional priority
),
);
}
}
When I passed the repository method to $visitor->setData() function, I get this error:
Your current code is failing because $ticketRepo = $event->getObject(); is returning the Ticket object as in the line above it, not the expected TicketRepository.
You will need to inject the EntityManager or Repository (if you have repositories as a service) into your TicketSerializationSubscriber::__construct() to retrieve the TicketRepository.
Assuming you have autowire enabled for the directory containing TicketSerializationSubscriber.
use Doctrine\ORM\EntityManagerInterface;
class TicketSerializationSubscriber implements EventSubscriberInterface
{
/**
* #var \Doctrine\ORM\EntityManager
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function onPostSerialize(ObjectEvent $event)
{
/** #var JsonSerializationVisitor $visitor */
$visitor = $event->getVisitor();
$ticket = $event->getObject();
if (!$ticket instanceof Ticket) {
//ensure object is of expected type
return;
}
/** #var TicketRepository $ticketRepo */
$ticketRepo = $this->em->getRepository(Ticket::class);
$visitor->setData('TESTING', $ticketRepo->findByTicketDeadline($ticket));
//...
}
//...
}
If not using autowire you will need to manually add it to your TicketSerializationSubscriber service declaration arguments.
#app/config/services.yml
services:
#...
TicketSerializationSubscriber:
public: false
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: kernel.event_subscriber }
#...
Be sure to clear and warmup the Symfony cache after making the changes.
I have jms serializer and in my class entity have annotation. I have case when I need apply jms serializer annotation only in some case. How to do this ? First I think need move annotation from entity class to uml maybe and create some handler where I can enable this annotation for this entity, in the rest of the time jms are not applicable to entity
my entity
/**
* #Annotation\ExclusionPolicy("all")
*/
class Application implements ContainsRecordedMessages
{
/**
* #var int
*
* #Annotation\Groups({
* "get_application"
* })
* #Annotation\Expose()
*/
private $id;
/**
* #var int
*
* #Annotation\Groups({
* "post_application"
* })
* #Annotation\SerializedName("company_id")
* #Annotation\Expose()
*/
private $companyId;
/**
* #var string
*
* #Annotation\Groups({
* "post_application"
* })
* #Annotation\SerializedName("channel_sale")
* #Annotation\Expose()
*/
private $channelSale;
and I have class manager for this entity where I want enable jms and the disable jms serializer annotation
class ApplicationManager
{
public function someFunction()
{
$this->enableJmsForEntity(Application::class);
//some logic
//
$this->disableJmsForEntity(Application::class);
}
}
my goal - that jms serializer don't work in serializer proccess for response. Only in my service, wehre I create entity with deserializer function, jms serializer annotation for entity enable. Because in this old project all respons look like this
return $this->json($application, Response::HTTP_CREATED);
vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php:114
protected function json($data, $status = 200, $headers = array(), $context = array())
{
if ($this->container->has('serializer')) {
$json = $this->container->get('serializer')->serialize($data, 'json', array_merge(array(
'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS,
), $context));
return new JsonResponse($json, $status, $headers, true);
}
return new JsonResponse($data, $status, $headers);
}
and after adding jms annotation we have problem, response in changing ..
I have try to create a Custom REST POST plugin in my Drupal 8.3.2 for get an external JSON and then create an article from that.
I have follow that guide: How to create Custom Rest Resources for POST methods in Drupal 8
And this is my code:
<?php
namespace Drupal\import_json_test\Plugin\rest\resource;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\node\Entity\Node;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Psr\Log\LoggerInterface;
/**
* Provides a resource to get view modes by entity and bundle.
*
* #RestResource(
* id = "tio_rest_json_source",
* label = #Translation("Tio rest json source"),
* serialization_class = "Drupal\node\Entity\Node",
* uri_paths = {
* "canonical" = "/api/custom/",
* "https://www.drupal.org/link-relations/create" = "/api/custom"
* }
* )
*/
class TioRestJsonSource extends ResourceBase {
/**
* A current user instance.
*
* #var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* Constructs a new TioRestJsonSource object.
*
* #param array $configuration
* A configuration array containing information about the plugin
instance.
* #param string $plugin_id
* The plugin_id for the plugin instance.
* #param mixed $plugin_definition
* The plugin implementation definition.
* #param array $serializer_formats
* The available serialization formats.
* #param \Psr\Log\LoggerInterface $logger
* A logger instance.
* #param \Drupal\Core\Session\AccountProxyInterface $current_user
* A current user instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
array $serializer_formats,
LoggerInterface $logger,
AccountProxyInterface $current_user) {
parent::__construct($configuration, $plugin_id,
$plugin_definition, $serializer_formats, $logger);
$this->currentUser = $current_user;
}
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container, array
$configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('import_json_test'),
$container->get('current_user')
);
}
/**
* Responds to POST requests.
*
* Returns a list of bundles for specified entity.
*
* #param $data
*
* #param $node_type
*
* #return \Drupal\rest\ResourceResponse
*
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function post($node_type, $data) {
// You must to implement the logic of your REST Resource here.
// Use current user after pass authentication to validate access.
if (!$this->currentUser->hasPermission('access content')) {
throw new AccessDeniedHttpException();
}
$node = Node::create(
array(
'type' => $node_type,
'title' => $data->title->value,
'body' => [
'summary' => '',
'value' => $data->body->value,
'format' => 'full_html',
],
)
);
$node->save();
return new ResourceResponse($node);
}
}
Now if i try to test this without passing a payload and modifing the return value in this way:
return new ResourceResponse(array('test'=>'OK'));
It's working!
But if i send a custom payload like this using my custom code above:
{
"title": [{
"value": "Test Article custom rest"
}],
"type": [{
"target_id": "article"
}],
"body": [{"value": "article test custom"}]
}
I recieve a 400 Error with: Symfony\Component\HttpKernel\Exception\BadRequestHttpException: The type link relation must be specified. in Drupal\rest\RequestHandler->handle() (line 103 of core/modules/rest/src/RequestHandler.php).
What's going Wrong?
Thx.
I have find a solution:
I have removed the annotation:
* serialization_class = "Drupal\node\Entity\Node",
Then i take care just for data in my post function:
/**
* Responds to POST requests.
*
* Returns a list of bundles for specified entity.
*
* #param $data
*
*
* #return \Drupal\rest\ResourceResponse
*
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function post($data) {
// You must to implement the logic of your REST Resource here.
// Use current user after pass authentication to validate access.
if (!$this->currentUser->hasPermission('access content')) {
throw new AccessDeniedHttpException();
}
return new ResourceResponse(var_dump($data));
The important thing is, when you use postman for example, is to add an header with Content-Type -> application/json:
Instead of Content-Type -> application/hal+json
With this configuration i can post any type of JSON and then manage it as i prefer.
Bye!
I have an independent Symfony bundle (installed with Composer) with entities and repositories to share between my applications that connect same database.
Entities are attached to every applications using configuration (yml shown):
doctrine:
orm:
mappings:
acme:
type: annotation
dir: %kernel.root_dir%/../vendor/acme/entities/src/Entities
prefix: Acme\Entities
alias: Acme
Well, it was the easiest way to include external entities in application, but looks a bit ugly.
Whenever I get repository from entity manager:
$entityManager->getRepository('Acme:User');
I get either preconfigured repository (in entity configuration) or default Doctrine\ORM\EntityRepository.
Now I want to override bundled (or default) repository class for a single entity. Is there any chance to do it with some configuration/extension/etc?
I think, the best looking way is something like:
doctrine:
orm:
....:
Acme\Entities\User:
repositoryClass: My\Super\Repository
Or with tags:
my.super.repository:
class: My\Super\Repository
tags:
- { name: doctrine.custom.repository, entity: Acme\Entities\User }
You can use LoadClassMetadata event:
class LoadClassMetadataSubscriber implements EventSubscriber
{
/**
* #inheritdoc
*/
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata
];
}
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
/**
* #var \Doctrine\ORM\Mapping\ClassMetadata $classMetadata
*/
$classMetadata = $eventArgs->getClassMetadata();
if ($classMetadata->getName() !== 'Acme\Entities\User') {
return;
}
$classMetadata->customRepositoryClassName = 'My\Super\Repository';
}
}
Doctrine Events
Entities are attached to every applications using configuration (yml shown):
Well, it was the easiest way to include external entities in application, but looks a bit ugly.
You can enable auto_mapping
Works for Doctrine versions <2.5
In addition to Artur Vesker answer I've found another way: override global repository_factory.
config.yml:
doctrine:
orm:
repository_factory: new.doctrine.repository_factory
services.yml:
new.doctrine.repository_factory:
class: My\Super\RepositoryFactory
Repository Factory:
namespace My\Super;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Repository\DefaultRepositoryFactory;
class RepositoryFactory extends DefaultRepositoryFactory
{
/**
* #inheritdoc
*/
protected function createRepository(EntityManagerInterface $entityManager, $entityName)
{
if ($entityName === Acme\Entities\User::class) {
$metadata = $entityManager->getClassMetadata($entityName);
return new ApplicationRepository($entityManager, $metadata);
}
return parent::createRepository($entityManager, $entityName);
}
}
No doubt implementing LoadClassMetadataSubscriber is a better way.
With current symfony 5.3 and doctrine 2.9.5
In your configuration define the service and doctrine.orm.repository_factory:
doctrine:
orm:
#Replace repository factory
repository_factory: 'MyBundle\Factory\RepositoryFactory'
services:
MyBundle\Factory\RepositoryFactory:
arguments: [ '#router', '#translator', '%kernel.secret%' ]
Add you MyBundle/Factory/RepositoryFactory.php file:
<?php declare(strict_types=1);
namespace MyBundle\Factory;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Repository\RepositoryFactory as RepositoryFactoryInterface;
use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* This factory is used to create default repository objects for entities at runtime.
*/
final class RepositoryFactory implements RepositoryFactoryInterface {
/**
* The list of EntityRepository instances
*
* #var ObjectRepository[]
*/
private $repositoryList = [];
/**
* The kernel secret
*
* #var string
*/
private $secret;
/**
* The RouterInterface instance
*
* #var RouterInterface
*/
private $router;
/**
* The TranslatorInterface instance
*
* #var TranslatorInterface
*/
private $translator;
/**
* Initializes a new RepositoryFactory instance
*
* #param RouterInterface $router The router instance
* #param TranslatorInterface $translator The TranslatorInterface instance
* #param string $secret The kernel secret
*/
public function __construct(RouterInterface $router, TranslatorInterface $translator, string $secret) {
//Set router
$this->router = $router;
//Set secret
$this->secret = $secret;
//Set translator
$this->translator = $translator;
}
/**
* {#inheritdoc}
*/
public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository {
//Set repository hash
$repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_hash($entityManager);
//With entity repository instance
if (isset($this->repositoryList[$repositoryHash])) {
//Return existing entity repository instance
return $this->repositoryList[$repositoryHash];
}
//Store and return created entity repository instance
return $this->repositoryList[$repositoryHash] = $this->createRepository($entityManager, $entityName);
}
/**
* Create a new repository instance for an entity class
*
* #param EntityManagerInterface $entityManager The EntityManager instance.
* #param string $entityName The name of the entity.
*/
private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository {
//Get class metadata
$metadata = $entityManager->getClassMetadata($entityName);
//Get repository class
$repositoryClass = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName();
//Return repository class instance
//XXX: router, translator and secret arguments will be ignored by default
return new $repositoryClass($entityManager, $metadata, $this->router, $this->translator, $this->secret);
}
}
Then define your MyBundle/Repository/EntityRepository.php:
<?php declare(strict_types=1);
namespace MyBundle\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository as BaseEntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* EntityRepository
*
* {#inheritdoc}
*/
class EntityRepository extends BaseEntityRepository {
/**
* The RouterInterface instance
*
* #var RouterInterface
*/
protected RouterInterface $router;
/**
* The table keys array
*
* #var array
*/
protected array $tableKeys;
/**
* The table values array
*
* #var array
*/
protected array $tableValues;
/**
* The TranslatorInterface instance
*
* #var TranslatorInterface
*/
protected TranslatorInterface $translator;
/**
* The kernel secret
*
* #var string
*/
protected string $secret;
/**
* Initializes a new LocationRepository instance
*
* #param EntityManagerInterface $manager The EntityManagerInterface instance
* #param ClassMetadata $class The ClassMetadata instance
* #param RouterInterface $router The router instance
* #param TranslatorInterface $translator The TranslatorInterface instance
* #param string $secret The kernel secret
*/
public function __construct(EntityManagerInterface $manager, ClassMetadata $class, RouterInterface $router, TranslatorInterface $translator, string $secret) {
//Call parent constructor
parent::__construct($manager, $class);
//Set secret
$this->secret = $secret;
//Set router
$this->router = $router;
//Set slugger
$this->slugger = $slugger;
//Set translator
$this->translator = $translator;
//Get quote strategy
$qs = $manager->getConfiguration()->getQuoteStrategy();
$dp = $manager->getConnection()->getDatabasePlatform();
//Set quoted table names
//XXX: remember to place longer prefix before shorter to avoid strange replacings
$tables = [
'MyBundle:UserGroup' => $qs->getJoinTableName($manager->getClassMetadata('MyBundle:User')->getAssociationMapping('groups'), $manager->getClassMetadata('MyBundle:User'), $dp),
'MyBundle:Group' => $qs->getTableName($manager->getClassMetadata('MyBundle:Group'), $dp),
'MyBundle:User' => $qs->getTableName($manager->getClassMetadata('MyBundle:User'), $dp),
//XXX: Set limit used to workaround mariadb subselect optimization
':limit' => PHP_INT_MAX,
"\t" => '',
"\n" => ' '
];
//Set quoted table name keys
$this->tableKeys = array_keys($tables);
//Set quoted table name values
$this->tableValues = array_values($tables);
}
}
Then simply extend it in MyBundle/Repository/UserRepository.php:
<?php declare(strict_types=1);
namespace MyBundle\Repository;
/**
* UserRepository
*/
class UserRepository extends EntityRepository {
}