I am using FOSuser with SonataUserBundle and I am trying to add a user to the Clients group everytime someone registers, but it doesnt work. I dont get any errors, but I am not adding the group either... I tried it in two ways:
1) I overwritten the registrationController and made the confirmAction save the new group like this:
/**
* Tell the user his account is now confirmed
*/
public function confirmedAction()
{
$repository = $em->getRepository('ApplicationSonataUserBundle:Group');
$group = $repository->findOneByName('Clients');
$em = $this->getDoctrine()->getEntityManager();
$user = $this->getUser();
$user->addGroup($group);
$this->em->flush();
$userManager = $this->get('fos_user.user_manager');
$userManager->updateUser($user);
}
}
2) Icreated an eventListener and made the groupping there:
<?php
namespace Application\Sonata\UserBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityManager;
/**
* Listener responsible to change the redirection at the end of the password resetting
*/
class GrouppingListener implements EventSubscriberInterface
{
protected $em;
protected $user;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
);
}
public function onRegistrationSuccess(FormEvent $event)
{
$this->user = $event->getForm()->getData();
$entity = $this->em->getRepository('ApplicationSonataUserBundle:Group')->findOneByName('Clients'); // You could do that by Id, too
$this->user->addGroup($entity);
$this->em->flush();
}
}
My group entity is being extended like this:
<?php
/**
* This file is part of the <name> project.
*
* (c) <yourname> <youremail>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Application\Sonata\UserBundle\Entity;
use Sonata\UserBundle\Entity\BaseGroup as BaseGroup;
/**
* This file has been generated by the Sonata EasyExtends bundle ( http://sonata-project.org/bundles/easy-extends )
*
* References :
* working with object : http://www.doctrine-project.org/projects/orm/2.0/docs/reference/working-with-objects/en
*
* #author <yourname> <youremail>
*/
class Group extends BaseGroup
{
/**
* #var integer $id
*/
protected $id;
/**
* Get id
*
* #return integer $id
*/
public function getId()
{
return $this->id;
}
}
None of these options worked... I did this based on other stackoverflow answers...Why wont it work?
You're missing persist in both of your cases. In order for an entity to become manageable, it needs to be persisted first.
$user->addGroup($group);
$this->em->flush();
change this in your controller action to:
$user->addGroup($group);
$this->em->persist($user);
$this->em->flush();
A paragraph from Doctrine manual:
An entity can be made persistent by passing it to the EntityManager#persist($entity) method. By applying the persist operation on some entity, that entity becomes MANAGED, which means that its persistence is from now on managed by an EntityManager. As a result the persistent state of such an entity will subsequently be properly synchronized with the database when EntityManager#flush() is invoked.
Related
I'm learning about Entities and Repositories in Symfony and I want to know if is possible access entity object in its repository.
I have the following code in Controller
$account = new Account($username, $password, $email);
$em = $this->getDoctrine()->getManager();
$result = $em->getRepository('AppBundle:Account')->registerAccount();
and then in Repository
public function registerAccount() {
// How access to $account here?
}
Should I just pass $account to repository function or is there another way?
Yes, you need to pass that class instance as an argument.
public function register(Account $account)
{
//$account is accessible here...
}
P.S. I believe you want a method to persist your Account entities right? Its fine to have a method for that in your repositories.
I would use something like
public function save(Account $account)
{
$em = $this->getEntityManager();
$em->persist($account);
$em->flush();
return $account;
}
Repository is not a place where you should have an access into your entity instance
.
Following the documentation http://symfony.com/doc/current/doctrine/repository.html
In repository you should create a custom function which return a result you need (based on DQL you create), and then call your function on repository in controller.
First you have to tell your entity you create repository
// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
*/
class Product
{
//...
}
This information is in annotation which describe your entity.
Then you create your custom function
// src/AppBundle/Repository/ProductRepository.php
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery(
'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC'
)
->getResult();
}
}
As you can see the function findAllOrderedByName() is finding all item in table with product and order asc result.
And finally in a place when you need to have your result (eg. in a Controller) you need to call your repository using created function:
$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('AppBundle:Product')
->findAllOrderedByName();
I would like to suggest create object manager service like AccountManager which dependence EntityRepository and Entity Class name. This service can include create, update etc functions. After creating this service don't forgot to register it in services.
E.g.
in controller you can create object Account entity
$account = new Account();
$account->setUsername($username);
$account->setPassword($password);
$account->setEmail($email);
$accountManager = $this->get('account.manager');
$accountManager->registerAccount($account);
Object Manager service
<?php
//...
class AccountManager
{
/**
* #var EntityManager
*/
private $entityManager;
/*
* #var string
*/
private $entityClassName;
/**
* #param EntityManager $entityManager
* #param string $entityClassName
*/
public function __construct(EntityManager $entityManager, $entityClassName)
{
$this->entityManager = $entityManager;
$this->entityClassName = $entityClassName;
}
/**
* #param EntityManager $entityManager
* #return bool
*/
public function registerAccount(Account $account)
{
try {
$this->entityManager->persist($account);
$this->entityManager->flush();
return true;
} catch (\Exception $exception) {
// logging
}
return false;
}
}
`
I have a lifecycle event. As soon as an order is created the prePersist lifecycle event add a few more details to the order before it is persisted to the database.
This is my prePersist event class;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Qi\Bss\BaseBundle\Entity\Business\PmodOrder;
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* Listener class
* Handles events related to list prices
*/
class OrderUserListener
{
/**
* Service container
* #var type
*/
private $serviceContainer;
/**
* Performs tasks before destruction
* #ORM\PrePersist
*/
public function prePersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$user = $this->serviceContainer->get('security.token_storage')->getToken()->getUser();
if ($user) {
$order->setCreatedBy($user);
$order->setCreatedAt(new \DateTime(date('Y-m-d H:i:s')));
$order->setDepartment($user->getDepartment());
$order->setStatus(PmodOrder::STATUS_AWAITING_APPROVAL);
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
}
/**
* Sets the sales order exporter object
* #param type $serviceContainer
*/
public function setServiceContainer($serviceContainer)
{
$this->serviceContainer = $serviceContainer;
}
}
It works perfectly but this part $this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created'); doesn't want to work. I try to call a service inside it. I know the service works perfectly inside my controllers, but here I get an error;
A new entity was found through the relationship
'Qi\Bss\BaseBundle\Entity\Business\PmodLog#order' that was not
configured to cascade persist operations for entity: Nuwe Test vir
logger. To solve this issue: Either explicitly call
EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example
#ManyToOne(..,cascade={"persist"}).
This is how my OrderLogger service class looks like;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Doctrine\ORM\EntityManager;
use Qi\Bss\BaseBundle\Entity\Business\PmodLog;
/**
* Class AppLogger. Purchase Module logger.
* #package FcConnectBundle\Lib
*/
class OrderLogger {
private $em;
private $tokenStorage;
/**
* Constructor.
*
* #param EntityManager $em
* #param TokenStorage $securityTokenStorage
*/
public function __construct(EntityManager $em, TokenStorage $securityTokenStorage)
{
$this->em = $em;
$this->tokenStorage = $securityTokenStorage;
}
/**
* Log an order action.
*
* #param string $text
*/
public function log($order, $action)
{
$logRecord = new PmodLog();
if (is_object($this->tokenStorage->getToken())) {
$user = $this->tokenStorage->getToken()->getUser();
if (is_object($user)) {
$logRecord->setUser($user);
}
}
$logRecord->setOrder($order);
$logRecord->setAction($action);
$logRecord->setTime(new \DateTime());
$this->em->persist($logRecord);
$this->em->flush();
}
}
I have already tried changing the persist in my log to merge, but that also doesn't work. Can somebody please help and explain what I do wrong?
This is not the best architecture, but it will work:
On prePersist add all messages to some kind of private variable (like $logMessages), and add another event
/**
* #param PostFlushEventArgs $args
*/
public function postFlush(PostFlushEventArgs $args)
{
$logMessages = $this->logMessages;
$this->logMessages = array(); //clean to avoid double logging
if (!empty($logMessages)) {
foreach ($logMessages as $message) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($message);
}
}
}
I fixed the problem by adding a postPersist and call the logger in there instead of inside my prePersist;
/**
* Performs tasks before destruction
* #ORM\PostPersist
*/
public function postPersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
Because what I think is happening is that the logger tries to be executed but the order in the logger doesn't yet exists as it is not yet persisted. This way makes more sense to me, and I think this is the easiest fix. I could be wrong though, any comments and other opinions on my answer are welcome.
I've converted a PHP application over to Symfony2 and have yet another structural question...
In my old application I would have entity classes that might act upon other entity classes...for instance, I have a search class and a result class. A function like search->updateSearch() would operate upon the search class, and upon its child result class ($this->result->setFoo('bar'). This is just one example of an entity-related function that doesn't belong in Symfony2's entity class.
From what I can tell it seems like the most symfonyesque method would be to create a service, something along the lines of a searchHelper class, to which I could pass the entity manager, $search, and $result classes, and operate on them there.
Does that sound like the best course of action?
Thank you!
For this scenario I use Model Managers, it's intended to be a business layer ORM agnostic interface for operating with entities. Something like:
<?php
/**
* Group entity manager
*/
class GroupManager
{
/**
* Holds the Doctrine entity manager for database interaction
* #var EntityManager
*/
protected $em;
/**
* Holds the Symfony2 event dispatcher service
* #var EventDispatcherInterface
*/
protected $dispatcher;
/**
* Entity specific repository, useful for finding entities, for example
* #var EntityRepository
*/
protected $repository;
/**
* Constructor
*
* #param EventDispatcherInterface $dispatcher
* #param EntityManager $em
* #param string $class
*/
public function __construct(EventDispatcherInterface $dispatcher, EntityManager $em)
{
$this->dispatcher = $dispatcher;
$this->em = $em;
$this->repository = $em->getRepository($class);
}
/**
* #return Group
*/
public function findGroupBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
/**
* #return Group
*/
public function createGroup()
{
$group = new Group();
// Some initialization or creation logic
return $group;
}
/**
* Update a group object
*
* #param Group $group
* #param boolean $andFlush
*/
public function updateGroup(Group $group, $andFlush = true)
{
$this->em->persist($group);
if ($andFlush) {
$this->em->flush();
}
}
/**
* Add a user to a group
*
* #param User $user
* #param Group $group
* #return Membership
*/
public function addUserToGroup(User $user, Group $group)
{
$membership= $this->em->getRepository('GroupBundle:Membership')
->findOneBy(array(
'user' => $user->getId(),
'group' => $group->getId(),
));
if ($membership && $membership->isActive()) {
return null;
} elseif ($membership && !$membership->isActive()) {
$membership->setActive(true);
$this->em->persist($membership);
$this->em->flush();
} else {
$membership = new Membership();
$membership->setUser($user);
$membership->setGroup($group);
$this->em->persist($membership);
$this->em->flush();
}
$this->dispatcher->dispatch(
GroupEvents::USER_JOINED_GROUP, new MembershipEvent($user, $group)
);
return $membership;
}
And then the service definition:
<service id="app.model_manager.group" class="App\GroupBundle\Entity\GroupManager">
<argument type="service" id="event_dispatcher" />
<argument type="service" id="doctrine.orm.entity_manager" />
</service>
You can inject the logger, mailer, router, or whichever other service you could need.
Take a look to FOSUserBundle managers, to get examples and ideas about how to use them.
It sounds like you should be using doctrines custom repository classes. You can check them out here: http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes
Basically they allow you to add custom logic above and beyond your basic entity. Also because they are basically an extension of the entity it makes it really easy to load them in and use their functions:
//Basic Entity File
/**
* #ORM\Entity(repositoryClass="Namespace\Bundle\Repository\ProductRepo")
*/
class Product
{
//...
}
Then the repo file for that entity:
//Basic Repo File
use Doctrine\ORM\EntityRepository;
class ProductRepo extends EntityRepository
{
public function updateSearch($passedParam)
{
// Custom query goes here
}
}
Then from your controller you can load the repo and use the function:
//Controller file
class ProductController extends Controller
{
public function updateSearchAction()
{
$productRepo = $this->getDoctrine()->getManager()->getRepository('Namespace\Bundle\Entity\Product');
// Set $passedParam to what ever it needs to be
$productRepo->updateSearch($passedParam);
}
}
I have added data fixtures in my project that relies on referencing entity objects from each other.
In data fixture one, I have added entity references such as:
// GroupEntity_Fixtures.php file
$this->addReference('GROUP_USER', $groupUser);
$this->addReference('GROUP_ADMIN', $groupAdmin);
Where $groupAdmin and $groupUser are both Group() entities. In my second fixtures file I want to add those entities to my User entity via:
//UserEntity_Fixtures.php file
$userActive->addGroup($this->getReference('GROUP_USER'));
$userActive is a User entity with a Many to Many relationship to Group Entity. Unfortunately it seems that I am only passing in a proxy of the entity and not the entity itself which renders the following error:
[Symfony\Component\Debug\Exception\ContextErrorException]
Catchable Fatal Error: Argument 1 passed to Blogger\BlogBundle\Entity\User:
:addGroup() must be an instance of Blogger\BlogBundle\Entity\groups, instan
ce of Proxies\__CG__\Blogger\BlogBundle\Entity\Group given, called in /home
/na/Practice/src/Blogger/BlogBundle/DataFixtures/ORM/CreateUserController_S
ignUpForm_UserEntity_Fixtures.php on line 27 and defined in /home/na/Practi
ce/src/Blogger/BlogBundle/Entity/User.php line 305
How do I convert the reference from a proxy to the entity it expects?
Code for Group Fixture:
<?php
// DataFixtures/ORM/GroupEntity_Fixtrues.php
namespace Blogger\BlogBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use Blogger\BlogBundle\Entity\User;
use Blogger\BlogBundle\Entity\Group;
class GroupEntity_Fixtures extends AbstractFixture implements OrderedFixtureInterface
{
/**
* {#inheritDoc}
*/
public function load(ObjectManager $manager)
{
$groupUser = new Group();
$groupUser->setName('GROUP_USER');
$groupUser->setRole('ROLE_USER');
$manager->persist($groupUser);
$groupAdmin = new Group();
$groupAdmin->setName('GROUP_ADMIN');
$groupAdmin->setRole('ROLE_USER,ROLE_ADMIN');
$manager->persist($groupAdmin);
$manager->flush();
$this->addReference('GROUP_USER', $groupUser);
$this->addReference('GROUP_ADMIN', $groupAdmin);
}
public function getOrder()
{
return 1;
}
}
Code for User Fixture
<?php
// DataFixtures/ORM/CreateUserController_SignUpForm_UserEntity_Fixtrues.php
namespace Blogger\BlogBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use Blogger\BlogBundle\Entity\User;
use Blogger\BlogBundle\Entity\Group;
class CreateUserController_SignUpForm_UserEntity_Fixtures extends AbstractFixture implements OrderedFixtureInterface
{
/**
* {#inheritDoc}
*/
public function load(ObjectManager $manager)
{
$groupUser2 = new Group();
$groupUser2->setName('GROUP_USER');
$groupUser2->setRole('ROLE_USER');
$manager->persist($groupUser2);
// This person represents an active (email verified) user.
$userActive = new User();
$userActive->setPassword("passwordActive");
$userActive->setEmail("testActive#test.com");
$userActive->setUserName("testActive");
$userActive->setPassword(crypt($userActive->getPassword(),$userActive->getSalt()));
$userActive->setEmailToken(md5(uniqid(rand(), true)));
$userActive->addGroup($groupUser2);
//$userActive->getGroups()->add($groupRepository->getGroupByName("BASIC_USER"));
// This person represents an unactive (email not verified) user.
$userUnactive = new User();
$userUnactive->setPassword("passwordUnactive");
$userUnactive->setEmail("testUnactive#test.com");
$userUnactive->setUserName("testUnactive");
$userUnactive->setPassword(crypt($userUnactive->getPassword(),$userUnactive->getSalt()));
$userUnactive->setEmailToken(md5(uniqid(rand(), true)));
// Persist objects into the database
$manager->persist($userActive);
$manager->persist($userUnactive);
$manager->flush();
}
public function getOrder()
{
return 2;
}
}
Code for Group Entity:
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="groups")
*/
private $users;
Code for User Entity:
/**
* #ORM\ManyToMany(targetEntity="Group", mappedBy="users")
*/
protected $groups;
Added Group Methos:
/**
* Add groups
*
* #param \Blogger\BlogBundle\Entity\groups $groups
* #return User
*/
public function addGroup(\Blogger\BlogBundle\Entity\groups $groups)
{
$this->groups[] = $groups;
return $this;
}
The addGroup method has the wrong type hint:
It should be:
/**
* Add groups
*
* #param \Blogger\BlogBundle\Entity\Group $groups
* #return User
*/
public function addGroup(\Blogger\BlogBundle\Entity\Group $groups)
{
$this->groups[] = $groups;
return $this;
}
Notice \Blogger\BlogBundle\Entity\Group instead of \Blogger\BlogBundle\Entity\groups.
I'm having this behavior with Doctrine 2.1 where I'm looking for a nice 'workaround'. The problem is as follows:
I have a user Entity:
/**
* #Entity(repositoryClass="Application\Entity\Repository\UserRepository")
* #HasLifecycleCallbacks
*/
class User extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var \DateTime
* #Column(type="datetime",nullable=false)
*/
protected $insertDate;
/**
*
* #var string
* #Column(type="string", nullable=false)
*/
protected $username;
/**
*
* #ManyToOne(targetEntity="UserGroup", cascade={"merge"})
*/
protected $userGroup;
}
And a usergroup entity:
/**
* #Entity
*/
class UserGroup extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var string
* #Column(type="string",nullable=false)
*/
protected $name;
}
If I instantiate a user object (doing this with Zend_Auth) and Zend_Auth puts it automatically the session.
The problem is however, that is I pull it back from the session at a next page then the data in the user class is perfectly loaded but not in the userGroup association. If I add cascade={"merge"} into the annotation in the user object the userGroup object IS loaded but the data is empty. If you dump something like:
$user->userGroup->name
You will get NULL back. The problem is no data of the usergroup entity is accesed before the user object is saved in the session so a empty initialized object will be returned. If I do something like:
echo $user->userGroup->name;
Before I store the user object in the session all data of the assocication userGroup is succesfully saved and won't return NULL on the next page if I try to access the $user->userGroup->name variable.
Is there a simple way to fix this? Can I manually load the userGroup object/association with a lifecycle callback #onLoad in the user class maybe? Any suggestions?
Your problem is a combination of what mjh_ca answered and a problem with your AbstractEntity implementation.
Since you show that you access entity fields in this fashion:
$user->userGroup->name;
I assume your AbstractEntity base class is using __get() and __set() magic methods instead of proper getters and setters:
function getUserGroup()
{
return $this->userGroup;
}
function setUserGroup(UserGroup $userGroup)
{
$this->userGroup = $userGroup;
}
You are essentially breaking lazy loading:
"... whenever you access a public property of a proxy object that hasn’t been initialized yet the return value will be null. Doctrine cannot hook into this process and magically make the entity lazy load."
Source: Doctrine Best Practices: Don't Use Public Properties on Entities
You should instead be accessing fields this way:
$user->getUserGroup()->getName();
The second part of your problem is exactly as mjh_ca wrote - Zend_Auth detaches your entity from the entity manager when it serializes it for storage in the session. Setting cascade={"merge"} on your association will not work because it is the actual entity that is detached. You have to merge the deserialized User entity into the entity manager.
$detachedIdentity = Zend_Auth::getInstance()->getIdentity();
$identity = $em->merge($detachedIdentity);
The question, is how to do this cleanly. You could look into implementing a __wakeup() magic method for your User entity, but that is also against doctrine best practices...
Source: Implementing Wakeup or Clone
Since we are talking about Zend_Auth, you could extend Zend_Auth and override the getIdentity() function so that it is entity aware.
use Doctrine\ORM\EntityManager,
Doctrine\ORM\UnitOfWork;
class My_Auth extends \Zend_Auth
{
protected $_entityManager;
/**
* override otherwise self::$_instance
* will still create an instance of Zend_Auth
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}
public function getEntityManager()
{
return $this->_entityManager;
}
public function setEntityManager(EntityManager $entityManager)
{
$this->_entityManager = $entityManager;
}
public function getIdentity()
{
$storage = $this->getStorage();
if ($storage->isEmpty()) {
return null;
}
$identity = $storage->read();
$em = $this->getEntityManager();
if(UnitOfWork::STATE_DETACHED === $em->getUnitOfWork()->getEntityState($identity))
{
$identity = $em->merge($identity);
}
return $identity;
}
}
And than add an _init function to your Bootstrap:
public function _initAuth()
{
$this->bootstrap('doctrine');
$em = $this->getResource('doctrine')->getEntityManager();
$auth = My_Auth::getInstance();
$auth->setEntityManager($em);
}
At this point calling $user->getUserGroup()->getName(); should work as intended.
When you store the entity to a session (via Zend_Auth or otherwise), the object is serialized and no longer maintained by Doctrine when subsequently retrieved and unserialized. Try merging the entity back into the EntityManager. See http://www.doctrine-project.org/docs/orm/2.1/en/reference/working-with-objects.html