How to use entity as service in doctrine (Using Symfony 2.1).
Example usage:
<?php
namespace MyNamespace;
class MyEntity
{
protected $container = NULL;
public function __construct($container)
{
$this->container = $container;
}
/**
* #ORM\PrePersist
*/
public function()
{
// Must call to container and get any parameters
// for defaults sets entity parameters
$this->container->get('service.name');
}
}
As a result, I need to get access to the entire container.
EDIT: THIS IS NOT THE PREFERRED WAY, it's the only way to get service container inside an entity, it's not a good practice, it should be avoided, but this just answers the question.
In case you still want the container and/or repository you can extend a base abastractEntity like this:
<?php
namespace Acme\CoreBundle\Entity;
/**
* Abstract Entity
*/
abstract class AbstractEntity
{
/**
* Return the actual entity repository
*
* #return entity repository or null
*/
protected function getRepository()
{
global $kernel;
if ('AppCache' == get_class($kernel)) {
$kernel = $kernel->getKernel();
}
$annotationReader = $kernel->getContainer()->get('annotation_reader');
$object = new \ReflectionObject($this);
if ($configuration = $annotationReader->getClassAnnotation($object, 'Doctrine\ORM\Mapping\Entity')) {
if (!is_null($configuration->repositoryClass)) {
$repository = $kernel->getContainer()->get('doctrine.orm.entity_manager')->getRepository(get_class($this));
return $repository;
}
}
return null;
}
}
An entity is a data model and should only hold data (and not have any dependencies on services). If you want to modify your model in case of a certain event (PrePersist in your case) you should look into making a Doctrine listener for that. You can inject the container when defining the listener:
services:
my.listener:
class: Acme\SearchBundle\Listener\YourListener
arguments: [#your_service_dependency_or_the_container_here]
tags:
- { name: doctrine.event_listener, event: prePersist }
Related
Given a class Publisher like this:
<?php
namespace App\Util\Publisher;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\MessageBusInterface;
class Publisher
{
protected $topic = null;
protected $bus;
/**
* Publisher constructor.
* #param MessageBusInterface $bus
*/
public function __construct(MessageBusInterface $bus)
{
$this->topic = getenv('TOPIC_MAIN_URL');
$this->bus = $bus;
}
...
}
I would like to autowire it in my controllers like this:
/**
* #Route("/_exp/_exp", name="exp")
*/
public function expAction(Publisher $publisher)
{
...
}
and I added this to my services.yaml:
Symfony\Component\Messenger\MessageBusInterface: ~
App\Util\Publisher\Publisher:
autowire: true
arguments: ['#Symfony\Component\Messenger\MessageBusInterface']
But I get an error:
Cannot instantiate interface Symfony\Component\Messenger\MessageBusInterface
Is that related to the MessageBusInterface or am I doing something wrong with the autowiring. I followed The Symfony docs for autowiring and they seem to be the same?
Thank you!
I believe MessageBusInterface service is already declared by Symfony Messenger component.
Try to remove Symfony\Component\Messenger\MessageBusInterface: ~ from your services.yaml, otherwise you are overriding the default definition.
A note for clarification: MessageBusInterface service does not really exists, it in an alias over the "default bus" service. You can declare other buses, cf documentation
According to this question How to load Symfony's config parameters from database (Doctrine) I have a similar problem. I need to set the parameter dynamically and I want to provide data from another custom service.
So, I have Event Listener which setting current account entity (by sub-domain or currently logged user)
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Doctrine\ORM\EntityManager;
use AppBundle\Manager\AccountManager;
use Palma\UserBundle\Entity\User;
/**
* Class CurrentAccountListener
*
* #package AppBundle\EventListener
*/
class CurrentAccountListener {
/**
* #var TokenStorage
*/
private $tokenStorage;
/**
* #var EntityManager
*/
private $em;
/**
* #var AccountManager
*/
private $accountManager;
private $baseHost;
public function __construct(TokenStorage $tokenStorage, EntityManager $em, AccountManager $accountManager, $baseHost) {
$this->tokenStorage = $tokenStorage;
$this->em = $em;
$this->accountManager = $accountManager;
$this->baseHost = $baseHost;
}
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
$accountManager = $this->accountManager;
$accountManager->setCurrentAccount( $this->getCurrentAccount($request) );
}
private function getCurrentAccount($request){
if($this->getCurrentAccountByLoggedUser()) {
return $this->getCurrentAccountByLoggedUser();
}
if($this->getCurrentAccountBySubDomain($request) ) {
return $this->getCurrentAccountBySubDomain($request);
}
return;
}
private function getCurrentAccountBySubDomain($request) {
$host = $request->getHost();
$baseHost = $this->baseHost;
$subdomain = str_replace('.'.$baseHost, '', $host);
$account = $this->em->getRepository('AppBundle:Account')
->findOneBy([ 'urlName' => $subdomain ]);
if(!$account) return;
return $account;
}
private function getCurrentAccountByLoggedUser() {
if( is_null($token = $this->tokenStorage->getToken()) ) return;
$user = $token->getUser();
return ($user instanceof User) ? $user->getAccount() : null;
}
}
services.yml
app.eventlistener.current_account_listener:
class: AppBundle\EventListener\CurrentAccountListener
arguments:
- "#security.token_storage"
- "#doctrine.orm.default_entity_manager"
- "#app.manager.account_manager"
- "%base_host%"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
And very simply account manager with setter and getter only. If I need access to current account I call
$this->get('app.manager.account_manager')->getCurrentAccount();
Everything works fine.
Now I am trying set some parameter from my service with compiler pass
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ParametersCompilerPass implements CompilerPassInterface {
const ACCOUNT_MANAGER_SERVICE_ID = 'app.manager.account_manager';
public function process(ContainerBuilder $container) {
if(!$container->has(self::ACCOUNT_MANAGER_SERVICE_ID)) {
return;
}
$currentAccount = $container->get(self::ACCOUNT_MANAGER_SERVICE_ID)
->getCurrentAccount();
$container->setParameter(
'current_account', $currentAccount
);
}
}
AppBundle.php
namespace AppBundle;
use AppBundle\DependencyInjection\Compiler\ParametersCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ParametersCompilerPass(), PassConfig::TYPE_AFTER_REMOVING);
}
}
I got current_account as null every time, no matter what PassConfig I use. Any ideas?
Thank you for your attention.
Compilation pass are executed when you run Symfony for the first time (CLI command or first http request). Once the cache is build (compiled) this code it never gets executed again.
Solution with parameters [I do not recommend this]
If your parameter can change from one to another HTTP Request you should not use a parameter as some services may be initialized before your parameter is ready and others after. Although if this is the way you want to go, you can add an event that listens to the kernel request and modify/set the parameter there. Have a look at https://symfony.com/doc/current/components/http_kernel.html#component-http-kernel-event-table
Current account in the user / session
If currentAccount depends on the user logged, why you do not store that info in the user or session and access to it from your services?
I'm trying to get Entity Listeners work with ODM in Symfony 2.7 but to no avail.
a51.document.listener.store:
class: A51\FilesystemBundle\EventListener\StoreEntityListener
tags:
- { name: doctrine.odm.mongodb.document_manager, event: postLoad, method: onPostLoad }
arguments: [#a51.repo.file]
and:
<?php
namespace A51\FilesystemBundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use A51\FilesystemBundle\Document\Store;
use A51\FilesystemBundle\Repository\FileRepositoryInterface;
class StoreEntityListener
{
/**
* #var FileRepositoryInterface
*/
private $fileRepository;
public function __construct(FileRepositoryInterface $fileRepository)
{
$this->fileRepository = $fileRepository;
}
public function onPostLoad(LifecycleEventArgs $args)
{
$this->index($args);
}
public function index(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Store)
{
$entity->setTotalByteSize($this->fileRepository->findSumFilesSizeByStore($entity));
}
}
}
I've tried pretty much everything I could find in docs but for some reason onPostLoad method does not get called.
Store document gets loaded with ParamConverter:
* #ParamConverter("store", class="A51FilesystemBundle:Store")
Any help would be welcome.
I have a MongoDB listener in my project, but my code it's so different of yours. There's a simpler way, all you must to do it's import DocumentManager and then you can call it from construct to use it on all you listener. I'm gonna show you my code and tell me if this help you ;)
namespace AppBundle\OdmListener;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ODM\MongoDB\DocumentManager;
class RedundancyListener
{
/**
* #var DocumentManager
*/
private $dm;
/**
* #param DocumentManager $odm
*/
function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
Then inside you can do any queries or updates as you do it in you controller. Also you can use ORM or ODM CycleEvents if you import them, like I do in the example.
/**
* #param LifecycleEventArgs $eventArgs
*/
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof \AppBundle\Entity\Example) {
$subscriptionHash = $this->getSubscription($entity);
$this->dm->createQueryBuilder('AppBundle\Document\Example')
->update()
//->multiple(true)
->field('name')->set($entity->getName())
->field('last_name')->set($entity->getLastName())
->field('mail')->set($entity->getMail())
->getQuery()
->execute();
}
}
}
I'm trying to inject my repository service into EventListener but that leads me to following exception, which, with my basic knowledge of Symfony2, I have no idea how to resolve. Exception is:
ServiceCircularReferenceException in bootstrap.php.cache line 2129:
Circular reference detected for service "doctrine.orm.default_entity_manager", path: "doctrine.orm.default_entity_manager -> doctrine.dbal.default_connection -> person.connect.listener -> tag.repository.service".
And here is how I've declared repository and listener:
tag.repository.service:
class: Application\Bundle\PersonBundle\Entity\TagRepository
factory: ["#doctrine", getRepository]
arguments: [ Application\Bundle\PersonBundle\Entity\Tag ]
person.connect.listener:
class: Application\Bundle\PersonBundle\EventListener\ConnectListener
arguments:
tokenStorage: "#security.token_storage"
tagRepo: "#tag.repository.service"
tags:
- { name: doctrine.event_listener, event: postPersist, connection: default }
Most answers, that I've able to find, suggest injecting service container, but I really don't want do that. Is there any way to resolve this properly?
UPD: Here is the code of the listener. Everything worked fine until I've tried to inject TagRepository
class ConnectListener
{
/**
* #var TokenStorage
*/
private $tokenStorage;
/**
* #var TagRepository
*/
private $tagRepo;
/**
* #param TokenStorage $tokenStorage
* #param TagRepository $tagRepo
*/
public function __construct(TokenStorage $tokenStorage, TagRepository $tagRepo)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param LifecycleEventArgs $args
* #return void
*/
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Person) {
$user = $this->tokenStorage->getToken()->getUser();
$visibility = new PersonVisibility($entity, $user);
$visibility->setVisibilityType(PersonVisibility::VT_CREATED);
$entityManager->persist($visibility);
$entityManager->flush();
}
}
}
As far as TagRepository is descendant of EntityRepository try obtaining its instance in postPersist event. Like this:
// using full classname:
$tagRepo = $entityManager->getRepository("Application\Bundle\PersonBundle\Entity\TagRepository");
// alternatively:
$tagRepo = $entityManager->getRepository("ApplicationPersonBundle:Tag");
Yo can also change your declaration of your repository, don't use the factory and use one of these 2 methods.
This will avoid the circular reference and will be cleaner than use the EntityManager class.
I have the entity (such as below). I want to set some default values while creating.
As you can see in __construct, it is easy to set the $name (string), but how can I set the $group? (for example I know that there is a group in database with id=122)
/**
* #ORM\Entity
*/
class Person {
private $id;
/** #ORM\Column(type="string") */
private $name;
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
* #ORM\JoinColumn(referencedColumnName="id")
*/
private $group;
public function setGroup(Group $group)
{
$this->group = $group;
$group->addPerson($this);
}
// ... setters/getters
//construct default Person
public function __construct()
{
$this->setName("Mike");
$this->setGroup($EXISTING_GROUP_FROM_MY_DB); // <<--------------
}
}
I agree with moonwave99 that this is poor design. Here you are trying to access the database (through the Doctrine service) from a place that is not container-aware (i.e. it does not, and should not, know about Doctrine).
I had a similar issue recently... pretty much the same exact issue, actually. But I didn't want this logic to be inside the controller. So I wrote a service to take care of the User creation. And I gave that service access to the only other service it needed: Doctrine.
Here's an example, where a User is created with all available Roles:
namespace MyBundle\Entity;
class UserFactory
{
private $doctrine;
public function __construct($doctrine)
{
$this->doctrine = $doctrine;
}
public function generateNewUser($email, $password)
{
$user = new User();
// Since you have access to the Doctrine service, you can use $this->doctrine
// to do anything you would normally do in your controller with $this->getDoctrine()
$roles = $this->doctrine->getEntityManager()->getRepository("MyBundle:Role")->findAll();
foreach ($roles as $role)
{
$user->addRole($role);
}
return $user;
}
}
Now register that service in config.yml or services.yml, remembering to pass the Doctrine service to it:
services:
mybundle.factory.user:
class: MyBundle\Entity\UserFactory
arguments: ['#doctrine']
And that's it... Now, in your controller, you can create a new User by doing:
public function MyController()
{
$user = $this->get("mybundle.factory.user")->generateNewUser("someone#email.com", "password123");
}
The recommended method is to require the associated Entity object within the constructor arguments, optionally in combination with a Factory such as the Entity Repository, to supply the Group Entity during instantiation. This ensures the entity is always in a valid state.
src/Entity/Person.php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\PersonRepository")
*/
class Person
{
//...
public function __construct($name, Group $group)
{
$this->setName($name);
$this->setGroup($group);
}
//...
}
src/Repsotory/PersonRepository.php
namespace App\Repsotory;
use App\Entity\Group;
use App\Entity\Person;
class PersonRepository
{
const DEFAULT_GROUP = 122;
public function create($name, Group $group = null)
{
if (null === $group) {
$group = $this->_em->getReference(Group::class, self::DEFAULT_GROUP);
}
$person = new Person($name, $group);
$this->_em->persist($person);
return $person;
}
}
This allows you to rely solely on the Doctrine ORM Entity Manager to maintain the default Group association.
$person = $em->getRepository(Person::class)->create('Mike');
$group = $person->getGroup();
echo $group->getId(); //outputs: 122
$em->flush();
This approach can be extended upon in Symfony to use Query services instead of the doctrine entity repository, to provide a central location that handles the instantiation of the entities.
In Symfony 3.4+ you can use Repository
services
to provide dependency injection for the repository, instead of using
the EntityManagerInterface.
src/Service/PersonCreateQuery.php
namespace App\Service;
use App\Entity\Group;
use App\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
class PersonCreateQuery
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function __invoke($name)
{
$group = $this->em->getReference(Group::class, 122);
$person = new Person($name, $group);
$this->em->persist($person);
return $person;
}
}
Now you can use dependency injection to retrieve the Query service and use it as desired, such as with a Symfony Form or Controller.
namespace App\Controller;
use App\Service\PersonCreateQuery;
class PersonController
{
public function createAction(PersonCreateQuery $query)
{
$person = $query('Mike');
$this->getDoctrine()->getManager()->flush();
//...
}
}
Note: Usages of $em->getReference() can be replaced with $em->find(). Using $em->getReference() will prevent a query to the database but will throw an exception if the reference is invalid, while using $em->find() will return null instead.
Another approach is to use either Lifecycle Callbacks in the entity or an Event Listener to do more complex functionality. However, this will cause your entity to be instantiated in an invalid state until it is persisted.
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Person
{
const DEFAULT_GROUP = 122;
/** #ORM\Column(type="string") */
private $name = 'Mike';
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
* #ORM\JoinColumn(referencedColumnName="id")
*/
private $group;
//....
public function setGroup(Group $group)
{
$this->group = $group;
$group->addPerson($this);
}
/**
* #param LifecycleEventArgs $event
* #ORM\PrePersist
*/
public function onPrePersist(LifecycleEventArgs $event)
{
if (!$this->group instanceof Group) {
/** set default group if not specified */
$group = $event->getEntityManager()->getReference(Group::class, self::DEFAULT_GROUP);
$this->setGroup($group);
}
}
}
Now when you persist a Person entity it will add the group if it was not explicitly set elsewhere.
$person = new Person();
$person->setName('Foo Bar');
$em->persist($person); //persist or do nothing if already persisted
$group = $person->getGroup();
echo $group->getId(); //outputs: 122
$groupPerson = $group->getPerson();
echo $groupPerson->getName(); //outputs: Foo Bar
$em->flush(); //save to database
For sanity here are the links to the docs for the doctrine events:
Doctrine 2 - Events
Doctrine 2 - Lifecycle Callbacks
Symfony - Doctrine Lifecycle Callbacks