To make it simple, let's say I have two objects with one-to-many relation:
User --(1:n)--> Request
with User defined as
class User {
...
/** #OneToMany(targetEntity="Request", mappedBy="user", cascade={"all"}) */
private $request;
...
}
and Request defined as
class Request {
...
/** #ManyToOne(targetEntity="User", inversedBy="request", cascade={"persist"}) */
private $user;
...
}
Is it possible to create a method that removes all Requests associated with User from within User entity?
What I need is something like this:
class User {
....
public function removeAllMyRequests() {
foreach ($this->getAllMyRequests() as $req)
$this->em->remove($req);
}
....
}
But apparently I'm not supposed to invoke entity manager from within entity.
You can mark the association with "Orphan Removal":
/**
* #Entity
*/
class User
{
/**
* #OneToMany(
* targetEntity="Request",
* mappedBy="user",
* cascade={"all"},
* orphanRemoval=true
* )
*/
private $requests;
}
Any Request object removed from the User#requests collection will be marked for removal during the next EntityManager#flush() call.
To remove all items at once, you can simply use Doctrine\Common\Collections\Collection#clear():
public function removeAllMyRequests() {
$this->requests->clear();
}
I think you are looking for the "cascade" option : http://docs.doctrine-project.org/en/2.0.x/reference/working-with-associations.html#transitive-persistence-cascade-operations
Related
I've an entity Order, with a property events which should contain a list of all changes made to this entity.
The Order class:
<?php
/**
* #ORM\Entity
*/
class Order
{
// more stuff...
/**
* #ORM\OneToMany(
* targetEntity="OrderEvent",
* mappedBy="order",
* cascade={"persist", "merge"}
* )
*/
protected $events;
// more stuff...
}
The OrderEvent class:
/**
* #ORM\Entity
*/
class OrderEvent
{
// more stuff...
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="events")
* #ORM\JoinColumn(nullable=false)
*/
protected $order;
// more stuff...
}
class OrderLifecycle
{
public function preUpdate(Order $order, PreUpdateEventArgs $args)
{
$changes = $args->getEntityChangeSet();
if (!empty($changes)) {
$event = new OrderEvent();
$event->setOrder($order)
->setChanges($changes);
$order->addEvent($event);
return $event;
}
}
}
But according to the doctrine documentation, the preUpdate method should not be used to change associations.
What is the recommended way to do things like this one?
I am using Zend Framework 2, but I think that's not relevant.
I think in this case you could use PostUpdate event. In that case you are sure that the update action was successful and you can do what you want; add the new OrderEvent instance to your $events collection.
EDIT
You are not the first one implementing such thing. Maybe you should check existing examples and see how they deal with this (or even consider using it). For example the Gedmo Loggable solution.
With this extension you can mark entities as loggable with a simple #annotiation:
/**
* #Entity
* #Gedmo\Loggable
*/
class Order
{
// Your class definition
}
I have two entities - User and UserSettings. In User entity, I want to have UserSettings as an attribute. That would be OK, I would add a OneToOne relation but there's a problem - because UserSettings is an owning side of the relation, every time I load User entity, Doctrine has to load the UserSettings entity too.
Is there a way how to load User but not UserSettings?
I made maybe a weird solution - there's no relation between these entities and the settings are loaded by method of Facade. For example:
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string")
*/
private $name;
/** #var UserSettings */
private $settings;
public function __construct()
{
$this->settings = new UserSettings();
}
public function setSettings(UserSettings $settings)
{
$this->settings = $settings;
}
}
/**
* #ORM\Entity
*/
class UserSettings
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(name="user_id", type="integer")
*/
private $userId;
}
class UserFacade
{
/**
* #var EntityManager
*/
private $em; // is injected automatically by DI
public function loadSettings(User $user)
{
$settings = $this->em->getRepository("UserSettings")->findOneBy(array("userId" => $user->id));
$user->setSettings($settings);
}
}
$user = $em->find("User", 1);
// if I want user's settings
$userFacade->loadSettings($user); // now I can use $user->getSettings()->something;
Side note: UserFacade is a service class that manipulates with users' data like adding new user, editing, deleting etc. In my MVC application, controller classes communicate with Facades, not with EntityManager directly.
That's OK - settings are loaded only when I want to. However, there are two possible problems:
a) I don't think this is a clear way
b) When I want a list of users, I cannot JOIN a table where settings are, because entities are not associated, so I have to make an extra SQL for each user.
My question is - how to solve the problem with OneToOne relation? I don't have much experience with Doctrine, so it may be a stupid question - sorry for that.
Thanks!
I have a question about Doctrine 2 and the ability (or not?) to extend an association between to classes.
Best explained with an example :)
Let's say I have this model (code is not complete):
/**
* #Entity
*/
class User {
/**
* #ManyToMany(targetEntity="Group")
* #var Group[]
*/
protected $groups;
}
/**
* #Entity
*/
class Group {
/**
* #ManyToMany(targetEntity="Role")
* #var Role[]
*/
protected $roles;
}
/**
* #Entity
*/
class Role {
/**
* #ManyToOne(targetEntity="RoleType")
* #var RoleType
*/
protected $type;
}
/**
* #Entity
*/
class RoleType {
public function setCustomDatas(array $params) {
// do some stuff. Default to nothing
}
}
Now I use this model in some projects. Suddenly, in a new project, I need to have a RoleType slightly different, with some other fields in DB and other methods. Of course, it was totally unexpected.
What I do in the "view-controller-but-not-model" code is using services:
// basic configuration
$services['RoleType'] = function() {
return new RoleType();
};
// and then in the script that create a role
$role_type = $services['RoleType'];
$role_type->setCustomDatas($some_params);
During application initialization, I simply add this line to overwrite the default RoleType
$services['RoleType'] = function() {
return new GreatRoleType();
};
Ok, great! I can customize the RoleType call and then load some custom classes that do custom things.
But... now I have my model. The model says that a Role targets a RoleType. And this is hard-written. Right now, to have my custom changes working, I need to extend the Role class this way:
/**
* #Entity
*/
class GreatRole extends Role {
/**
* Changing the targetEntity to load my custom type for the role
* #ManyToOne(targetEntity="GreatRoleType")
* #var RoleType
*/
protected $type;
}
But then, I need to extend the Group class to target GreatRole instead of Role.
And in the end, I need to extend User to target GreatGroup (which targets GreatRole, which targets GreatRoleType).
Is there a way to avoid this cascade of extends? Or is there a best practice out there that is totally different from what I did?
Do I need to use MappedSuperClasses? The doc isn't very explicit...
Thanks for your help!
--------------------------- EDIT ---------------------------
If I try to fetch all the hierarchy from User, that's when I encounter problems:
$query
->from('User', 'u')
->leftJoin('u.groups', 'g')
->leftJoin('g.roles', 'r')
->leftJoin('r.type', 't');
If I want to have a "r.type" = GreatRoleType, I need to redefine each classes.
I have question about inserting entity into a database. I have two models:
class News {
/**
* #Column(type="string", length=100)
* #var string
*/
protected $title;
/**
* #ManyToOne(targetEntity="User", inversedBy="news")
* #JoinColumn(referencedColumnName="id")
*/
protected $author;
}
class User {
/**
* #Id #GeneratedValue #Column(type="integer")
* #var integer
*/
protected $id;
/**
* #OneToMany(targetEntity="News", mappedBy="author")
*/
protected $news;
public function __construct() {
$this->news = new \Doctrine\Common\Collections\ArrayCollection;
}
}
To add new news I must include both User and News classes (if they're in separate files, for ex. UserModel.php and NewsModel.php) and write a code:
$news = new News()
$news->setTitle('TEST title');
$news->setAuthor($database->find('User', 1));
$database->persist($news);
My question is: Is there any way to insert news without including User class?
You don't need to actually load the User.
Instead, you can use a reference proxy:
<?PHP
$news = new News()
$news->setTitle('TEST title');
$news->setAuthor($em->getReference('User',1));
$em->persist($news);
one other thing you could do (thinking in a more object-oriented kinda way) is add a method called addNews($news) on your user entity:
public function addNews($news) {
// you should check if the news doesn't already exist here first
$this->news->add($news);
$news->setAuthor($this);
}
and add cascade persist to your mapping:
/**
* #OneToMany(targetEntity="News", mappedBy="author", cascade={"persist"})
*/
protected $news;
then fetch your user, add the news, and merge the changes:
$news = new News()
$news->setTitle('TEST title');
$author = $database->find('User', 1);
$author->addNews($news);
//merge changes on author entity directly
$em->merge($author);
I preferr this approach because it gives you the opportunity to do extra checks or controls while adding the news, making for reusable and easy to read code
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