I have this situation:
Abstract Class:
abstract class AbstractBase
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #var integer
*/
protected $id;
/**
* #ORM\Column(type="datetime", name="updated_at")
* #var \DateTime $updatedAt
*/
protected $updatedAt;
/**
* #ORM\PreUpdate
*/
public function setUpdatedAt()
{
die('THIS POINT IS NEVER REACHED');
$this->updatedAt = new \DateTime();
}
}
Concrete Class:
/**
* #ORM\Entity(repositoryClass="Entity\Repository\UserRepository")
* #ORM\Table(name="users")
* #ORM\HasLifecycleCallbacks
*/
class User extends AbstractBase
{
// some fields, relations and setters/getters defined here, these all work as expected.
}
Then i call it in my controller like this:
$user = $this->em->find('Entity\User', 1);
// i call some setters here like $user->setName('asd');
$this->em->flush();
die('end');
Everything works as expected, so the id field from the abstract class gets created for the User entity, i can access it etc.
The problem is, that the line "die('THIS POINT IS NEVER REACHED')" is never reached. (Note the #ORM\PreUpdate) This means that lifecycleCallbacks are not called on
inherited objects. Is this a bug, or is there a reason for this?
Your abstract base class has to be anotated as Mapped Superclasses and include the HasLifecycleCallbacks-Annotation.
Further Information: Inheritance Mapping in the Doctrine Documentation.
/**
* #ORM\MappedSuperclass
* #ORM\HasLifecycleCallbacks
*/
abstract class AbstractBase
{
[...]
/**
* #ORM\PreUpdate
*/
public function setUpdatedAt()
{
$this->updatedAt = new \DateTime();
}
}
/**
* #ORM\Entity(repositoryClass="Entity\Repository\UserRepository")
* #ORM\Table(name="users")
*/
class User extends AbstractBase
{
// some fields, relations and setters/getters defined here, these all work as expected.
}
You have to annotate the base class with #ORM\HasLifecycleCallbacks, and the function with #ORM\preUpdate
You have a typo (PreUpdate should be preUpdate), also preUpdate isn't called on creation (only on update). So if you want it also be triggered on creation, you should add #ORM\prePersist.
While the accepted reply is correct for the general case, in this particular case (timestamp) you actually want to use the doctrine extension Timestampable as explained for example here Lifecycle Callback Issue When Extending FOSUserBundle User Entity
It is important that the MappedSuperclass with HasLifecycleCallbacks is in the same namespace or directory as their child Entities.
I had problems with life cycle callbacks when the MappedSuperclass was in one directory (Model) while the Entities were in another (Entity). Putting the MappedSuperclass in the same directory as the Entities (Entity) solved the issue.
Maybe i'm wrong but I don't think preUpdate isn't triggered when you persist an entity. You should have a #prePersist.
http://www.doctrine-project.org/docs/orm/2.0/en/reference/events.html
But still then i'm not sure this is going to work but you could try that. Else a workaround would be to overwrite the setUpdatedAt function and just call his parent one but that's a bit ugly.
Hope the #prePersist helps for you.
Perhaps you could this issue report as a reference how to setup your annotations? The testcase seems to be valid and matches your use case.
I think you have to annotate the base class with #ORM\HasLifecycleCallbacks
docs
Related
I am creating a simple CMS Bundle for my headless symfony backend and I'm trying to map Page to Page with parent and child relation(Many children to one parent) and I have this class mapped superclass to create reusable code, this is a minified sample on what I'm trying to archive:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\MappedSuperclass()
*/
class Test
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function getId()
{
return $this->id;
}
/**
* #ORM\ManyToOne(targetEntity="Ziebura\CMSBundle\Entity\Test")
*/
protected $parent;
public function getParent()
{
return $this->parent;
}
public function setParent($parent)
{
$this->parent = $parent;
}
}
Then I'm extending this class as a normal entity to create DB table
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Ziebura\CMSBundle\Entity\Test as BaseTest;
/**
* #ORM\Table(name="test")
* #ORM\Entity(repositoryClass="App\Repository\TestRepository")
*/
class Test extends BaseTest
{
}
The issue is that I'm getting this doctrine exception
Column name `id` referenced for relation from App\Entity\Test towards Ziebura\CMSBundle\Entity\Test does not exist.
I don't quite understand why it produces this error or is the thing that I'm trying to archive impossible, I already did relations on mapped superclasses but it was 2 or more tables and not just a single on. I already tried creating $children field but it didnt worked and still produced above error. Did anyone try to create something simmilar? I couldn't find anything about this in doctrine docs, only found how to map 2 different superclasses. I suppose the easy way out would be to specify the relation in App namespace not in the Bundle but that pretty much destroys the purpose of reusable code if I'd have to declare that in every project I use the bundle. I believe in stack let's figure this out. Thanks!
Lets read Doctrine docs about this: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/inheritance-mapping.html#inheritance-mapping
A mapped superclass is an abstract or concrete class that provides persistent entity state and mapping information for its subclasses, but which is not itself an entity. Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.
...
A mapped superclass cannot be an entity, it is not query-able and persistent relationships defined by a mapped superclass must be unidirectional (with an owning side only). This means that One-To-Many associations are not possible on a mapped superclass at all.
According to this:
MappedSuperclass cannot be Entity
Cannot have One-To-Many relationship - so if you are defining ManyToOne to same class then it creates also OneToMany on same class - which, as you read above, is forbidden.
For some reason only changing the full entity path in BaseTest resolved app throwing the exception and it works, if anyone would face same issue try changing
/**
* #ORM\ManyToOne(targetEntity="Ziebura\CMSBundle\Entity\Test")
*/
protected $parent;
public function getParent()
{
return $this->parent;
}
public function setParent($parent)
{
$this->parent = $parent;
}
To
/**
* #ORM\ManyToOne(targetEntity="Test")
*/
protected $parent;
public function getParent()
{
return $this->parent;
}
public function setParent($parent)
{
$this->parent = $parent;
}
If someone knows why it has to be like this I'd much appreciate a comment to my answer.
I've got a User Entity defined (mapping in yml)
namespace My\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class User
{
...
And I created a child class that inherits from that entity, so that I can add some custom validation methods and a few fields that I need but do not need to be persisted (e.g. ConfirmPassword, ConfirmEmail fields)
namespace My\SecondBundle\EditModels;
use My\CoreBundle\Entity\User;
class UserModel extends User
{
When the user submit a registration form, I map the request to a UserModel entity, and if it is valid I try to persist the user.
The following code throws an exception
$entityManager->persist($userModel);
//=>The class 'My\SecondBundle\EditModels\UserModel' was not found in the chain configured namespaces My\CoreBundle\Entity
Question: How can I persist $userModel (instance of UserModel) as a User entity class? Possible options:
Do not use an inherited class and add custom fields and validation method to the User entity itself
Copy the fields from the UserModel to the User entity and persist the user entity
I don't think I should use Doctrine inheritance mechanism as I do not want to save the extra fields.
Thank you
I think your problem here, is that you've just configured My\CoreBundle\Entity namespace in Doctrine2, but the entity you actually want to persist is located in My\SecondBundle\EditModels.
Usually when inheriting classes marked as #ORM\Entity() the class you are extending from must have the class annotation #ORM\MappedSuperclass(). But normally you use this for single table inhertiance e.g., not for your usecase.
In my opinion the approach to split database related attributes from the others, is not affordable. I would keep validation related stuff in the model itself - you need it in your create/update action.
I'm not familiar with XML configuration, but when using annotations you need to mark each property to be mapped with database (using #ORM\Column()). So Doctrine will ignore all the other attributes and methods entirely.
So here I share my recently developed AbstractModel for you, to see how I've implemented validation (with respect/validation):
<?php
namespace Vendor\Package\Model;
use Doctrine\ORM\Mapping as ORM;
/**
* Abstract Model
*
* #ORM\MappedSuperclass()
*/
abstract class AbstractModel
{
/**
* #var \Respect\Validation\Validator
*/
protected $validator;
/**
* AbstractModel constructor
*/
public function __construct()
{
$this->validator = static::validation();
}
/**
* Defines validation for this model
*
* #return \Respect\Validation\Validator
*/
public static function validation() : \Respect\Validation\Validator
{
return \Respect\Validation\Validator::create();
}
/**
* Executes validations, defined in validation method.
*
* #return bool
*/
public function isValid() : bool
{
if (is_null($this->validator)) {
$this->validator = new \Respect\Validation\Validator();
$this->validation();
}
return $this->validator->validate($this);
}
}
A model which extends from the AbstractModel needs to implement a static validate method, to define class validation:
<?php
namespace Vendor\Package\Model;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\Table(name="my_model")
*/
class MyModel extends AbstractModel
{
/**
* #var string
* #ORM\Column(type="string")
*/
private $name;
/**
* Defines validation for this model
*
* #return \Respect\Validation\Validator
*/
public static function validation() : \Respect\Validation\Validator
{
return \Respect\Validation\Validator::create()
->attribute('name', \Respect\Validation\Validator::notEmpty()->stringType()->length(null, 32))
;
}
// getter, setter, ...
}
Each entity, persisted to database, will have the $validator property and all these methods, but because I left annotations here (and pretty sure this also works with xml/yaml) Doctrine ignores it.
And this way you also keep validation related stuff out of the model class itself, which is good for readability. The validation itself should be defined in the model itself, imho. But this respect/validation framework is neat way to achive this. Hope this helps :)
I have problem with Doctrine mapping. First of all I'll introduce my two entites:
/**
* #ORM\Entity
* #ORM\Table(name="header_fotos")
*/
class HeaderFoto extends BaseEntity{
....
/**
*
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="\App\Webpage\Webpage", inversedBy="headerFotos")
* #ORM\JoinColumn(name="webpage_id", referencedColumnName="id")
*/
protected $webpage;
...
}
Second entity:
/**
* #ORM\Entity
* #ORM\Table(name="webpages")
*/
class Webpage extends BaseEntity{
...
/**
* #ORM\OneToMany(targetEntity="\App\Webpage\HeaderFoto", mappedBy="webpage")
*/
protected $headerFotos;
public function __construct() {
parent::__construct();
$this->headerFotos = new ArrayCollection();
}
My problem is with mapping. When I load entity Webpage and try to access all entities of type HeaderFoto it cannot find any relation. I was trying to compare with another working project with Doctrine and everything is the same.
I was trying to change association to OneToOne on the both sides, just for sure. But in this case it returned Exception No mapping found for field webpage.
I will appreciate every help and advice. Thanks for help.
EDIT
To be more specific, it has problem in class \Doctrine\ORM\Persisters\BasicEntityPersister.php in method getOneToManyStatement. It tries to load associated objects, but the array associationMappings in this class is empty. This is the last thing what I have found out.
I have decided to create a 'MappedSuperclass' which will be extended by all other entities to easily share between them two common fields, it looks like this:
/**
* #ORM\MappedSuperclass
*/
abstract class EntityBase {
/**
* #ORM\Column(type="datetime")
*/
protected $created_at;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
protected $updated_at;
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updateTimestamps() {
$this->updated_at = new \DateTime('now');
if(is_null($this->created_at)) {
$this->created_at = new \DateTime('now');
}
}
}
My problem is that, after using generate:entities command, script copies to all child classes both fields (as private) as well as updateTimestamps function (which is empty).
Is there any possibility to prevent this behavior? Now i have only 5 entities so its not a big deal to manually delete unneeded code, but it may be pain when project grows up to 20 or more entities.
Or maybe it not the right way to achieve my needs?
If you define a class as abstract it will be interpreted as 'to be implemented'. Thats why you get dummy functions to implement the feature in the actual class. You should just extent a regular class to inherit the fields and the function to update the timestamps.
If you inherit entity classes, you must set the properties of the parent class as private. Child classes may only access them via the getter/setter. I had the same problem a while ago, making properties private works like a charm.
I have added #ORM\Entity and #ORM\HasLifecycleCallbacks annotations in my entity class and later in the entity class I have added the callback method with the right annotation (as following):
/**
*
* #ORM\PreUpdate
* #ORM\PrePersist
*/
protected function PreUpdateHandler()
{
echo '*********** PRE UPDATE *************';
var_dump('*********** PRE UPDATE *************');
return $this;
}
But the PreUpdateHandler is not getting called upon any DB manipulation (insert, update or delete). Any idea what am I missing?
BTW: Where can I see the list of all available event annotations (likes of #ORM\PreUpdate and #ORM\PrePersist)?
IMPORTANT!!!
My entity class is inherited from a base Entity class which is located in another directory (and namespace). I have added the HasLifecycleCallbacks annotation in the metadata of the base class as well. Does this matter for the callbacks to trigger?
Your lifecycle callback method PreUpdateHandler is not invoked because the visibility of method is protected and hence not accessible by the ORM. Change the visibility to public and try.
Try this and check your result
$em->persist($object);
$object->PreUpdateHandler()
$em->flush();
Replace your annotation callback
/**
*
* #ORM\PreUpdate
* #ORM\PrePersist
*/
To
/**
*
* #ORM\PreUpdate()
* #ORM\PrePersist()
*/
SO the thing is that I am using the yml file as well as the entity class and it seems as if the annotations doesn't work in parallel to the yml file. I removed the annotations and added the callbacks in the yml file and its working.