I have an entity in Doctrine2 and use the HasLivecycleCallbacks with PrePersist. In general this works fine, but I would like to change the version only, when certain fields in my entity change. Do I have a chance to get the old Values? Or just the keys that have been changed?
/**
* #ORM\HasLifecycleCallbacks
*/
class Person {
/**
* #PrePersist
* #PreUpdate
*/
public function increaseVersion() {
if ( $this->version == null ) {
$this->version = 0;
}
// only do this, when a certain attribute changed
$this->version++;
}
}
It depends on which LifecycleEvent we are talking about. PrePersist and PreUpdate are different events.
PreUpdate is fired before an Entity is, well, updated. This will give you a PreUpdateEventArgs object, which is an extended LifecycleEventArgs object. This will allow you to query for changed fields and give you access to the old and new value:
if ($event->hasChangedField('foo')) {
$oldValue = $event->getOldValue('foo');
$newValue = $event->getNewValue('foo');
}
You could also get all the changed field values through getEntityChangeSet(), which would give you an array like this:
array(
'foo' => array(
0 => 'oldValue',
1 => 'newValue'
),
// more changed fields (if any) …
)
PrePersist, on the other hand, assumes a fresh Entity (think insert new row). In PrePersist, you'll get a LifecycleEventArgs object which only has access to the Entity and the EntityManager. In theory, you can get access to the UnitOfWork (which keeps track of all the changes to Entities) through the EntityManager, so you could try to do
$changeSet = $event->getEntityManager()->getUnitOfWork()->getEntityChangeSet(
$event->getEntity()
);
to get the changes for the to be persisted Entity. You could then check this array for changed fields. However, since we are talking about an insert and not an update, I assume all fields wil be considered "changed" and the old values will likely be all null. I am not sure, this will work as you need it.
Further reference: http://docs.doctrine-project.org/en/2.0.x/reference/events.html
Related
I am trying to deserialize a json payload into an Entity, I need the objects inside this array to be owned by the parent object. Example payload.
{
"exportOwner": "test#example.com",
"uniqueIdentifier": "BCD99340-70D9-4E23-91DD-C40DFB64EC9D",
"events": [
{
"uniqueIdentifier": "E09FDE82-4CAA-4641-87AF-6C092D6E71C1"
}
]
}
When sent, that particular event either exists (using the uniqueIdentifier) or it does not. If it does not, I create it. Problem is, the parent object that holds this array basically owns these, so a OneToMany relationship exists.
So I need to iterate over that array of events and called setParentId() to that of the newly created parent object. Which I assume I need to do in this block of code somewhere.
$data = $request->getContent();
$entity = $serializer->deserialize($data, $this->type, 'json');
$entityManager = $this->getDoctrine()->getManager();
$repo = $this->getDoctrine()->getRepository($this->type);
// If it exists update it
if($existingEntity = $repo->findOneBy(
array(
"uniqueIdentifier" => $entity->getUniqueIdentifier()
)
)){
$entity->setId($existingEntity->getId());
$entityManager->merge($entity);
$entityManager->flush();
return $this->_200response($this->type." with uuid '".$existingEntity->getUniqueIdentifier()."' already exists. It has been updated");
}
// Create
$user = $this->getUserByEmail($entity->getExportOwner());
// $entity->setExportOwnerId($user->getId());
// $entity->setExportOwner(null);
$entityManager->persist($entity);
$entityManager->flush();
return $this->_200response($this->type." with uuid '".$entity->getUniqueIdentifier()."' created");
However, doing $entity->getEvents() is returning an empty array.
The relationship looks like this:
// Animal.php
/**
* #var Event[]|null
*
* #ORM\OneToMany(targetEntity="Animal", mappedBy="animal")
*/
private $events;
------------------------------------------------------------------
// Event.php
/**
* #ORM\ManyToOne(targetEntity="Animal", inversedBy="metrics")
*/
private $animal;
Even once I send the payload, and then recieved the object back, the events array is empty, yet the event exists.
This works 100% fine if I use the #ORM\JoinTable annotation with a ManyToMany relationship, but I don't really need that. I have one Animal to Many events.
Disclaimer: n00b to Doctrine, not ORMs in general, or the data mapper pattern, just Doctrine. Not sure if I'm missing something (references to the documentation are welcome if they're specific! I've read thoroughly though) or there is another preferred approach. A big part of this question is how to do it the right way, not necessarily how to do it in general.
The deets: I've got a one to many association between two entities: Calendar and Event. One calendar can have many events. Relevant code:
Calendar Entity
<?php namespace MyPackage\Src {
use \Doctrine\Common\Collections\ArrayColleciton;
class Calendar {
/**
* #OneToMany(targetEntity="MyPackage\Src\Event", mappedBy="calendarInstance", cascade={"all"})
*/
protected $associatedEvents;
public function __construct(){
$this->associatedEvents = new ArrayCollection();
}
public function addEvent( Event $eventObj ){
$eventObj->setCalendarInstance($this);
$this->associatedEvents->add($eventObj);
}
public function getEvents(){
return $this->associatedEvents;
}
// Assume entityManager() returns... an entity manager instance
public function save(){
$this->entityManager()->persist($this);
$this->entityManager()->flush();
return $this;
}
}
}
Event Entity
<?php namespace MyPackage\Src {
class Event {
/**
* #ManyToOne(targetEntity="MyPackage\Src\Calendar", inversedBy="associatedEvents")
* #JoinColumn(name="calendarID", referencedColumnName="id", nullable=false)
*/
protected $calendarInstance;
public function setCalendarInstance( Calendar $calObj ){
$this->calendarInstance = $calObj;
}
// Assume entityManager() returns... an entity manager instance
public function save(){
$this->entityManager()->persist($this);
$this->entityManager()->flush();
return $this;
}
}
}
Semantics of the ORM state that "Doctrine will only check the owning side of an association for changes" (<- proof of reading docs). So doing the following works fine when creating a new event (assuming cascade persist is enforced by the Calendar, which I have set with "all"), and then gett'ing the ArrayCollection from the calendar instance.
<?php
// Assume this returns a successfully persisted entity...
$cal = Calendar::getByID(1);
// Create a new event
$event = new Event();
$event->setTitle('Wowza');
// Associate the event to the calendar
$cal->addEvent($event);
$cal->save();
// Yields expected results (ie. ArrayCollection is kept in-sync)
echo count($cal->getEvents()); // "1"
Meow then... Lets say for some funny reason I want to create an association from the event side, like so:
<?php
// Assume this returns a successfully persisted entity...
$cal = Calendar::getByID(1);
// Create a new event
$event = new Event();
$event->setTitle('Dewalt Power Tools');
// Associate VIA EVENT
$event->setCalendarInstance($cal);
$event->save();
// ArrayCollection in the Calendar instance is out of sync
echo count($cal->getEvents()); // "0"
Persisting the event in this manner does work (it gets saved to the DB) - but now the entity manager is out of sync when trying to access associated events from the Calendar. Further, its understandable why: in the setCalendarInstance() method of the Event class, all thats happening is
$this->calendarInstance = $calendar;
I've tried the following (which actually works, it just feels really naughty)
$this->calendarInstance = $calendar;
if( ! $this->calendarInstance->getEvents()->contains($this) ){
$this->calendarInstance->getEvents()->add($this);
}
BUT, this feels very strongly like I'm breaking proper encapsulation, which is important to me. Am I being silly, and its OK to ->add() to the returned ArrayCollection of the Calendar instance, outside of the Calendar class? (Thats what I mean this feels wrong; the ArrayCollection is a protected property of Calendar and I feel like it shouldn't be modified externally).
---- OR ----
Should I enforce creating the association only from the owning side (the Calendar), and not allow doing $event->setCalendarInstance() and then saving the event. Which again brings up another point on encapsulation, that setCalendarInstance must be a public method in order for the Calendar's addEvents() method to work (so how would I prevent some poor soul inheriting my codebase from doing it improperly)?
Thanks, interested to hear approaches to this.
** Edit **
Further funny business: Going the route of creating a new event, passing the calendar instance, and then persisting the new event - here is where discrepancies seem to exist:
// Get an existing Calendar that has one Event
$calObj = Calendar::getByID(1);
echo count($calObj->getEvents()); // "1"
// Add from event side
$event = new Event();
$event->setTitle('Pinkys Brain');
$event->setCalendarInstance($calObj);
$event->save();
// I would think this would be "2" now...
echo count($calObj->getEvents()); // Still "1"
BUT... this works, and feels shloppy.
// Get an existing Calendar that has one Event
$calObj = Calendar::getByID(1);
echo count($calObj->getEvents()); // "1"
// Add from event side
$event = new Event();
$event->setTitle('Pinkys Brain');
$event->setCalendarInstance($calObj);
$event->save();
// Clear the entity manager on the existing $calObj
$calObj->entityManager()->clear();
// Now get a NEW instance of the same calendar
$calObjAgain = Calendar::getByID(1);
echo count($calObjAgain->getEvents()); // "2"
I am having annoying problems with persisting an entity with one or more OneToMany-Childs.
I have a "Buchung" entity which can have multiple "Einsatztage" (could be translated to an event with many days)
In the "Buchung entity I have
/**
* #param \Doctrine\Common\Collections\Collection $property
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung", cascade={"all"})
*/
private $einsatztage;
$einsatztage is set to an ArrayCollection() in the __constructor().
Then there is the "Einsatztag" Entity which has a $Buchung_id variable to reference the "Buchung"
/**
* #ORM\ManyToOne(targetEntity="Buchung", inversedBy="einsatztage", cascade={"all"})
* #ORM\JoinColumn(name="buchung_id", referencedColumnName="id")
*/
private $Buchung_id;
Now If I try to persist an object to the database the foreign key of the "Einsatztag" Table is always left empty.
$buchung = new Buchung();
$buchung->setEvent( $r->request->get("event_basis"));
$buchung->setStartDate(new \DateTime($r->request->get("date_from")));
$buchung->setEndDate(new \DateTime($r->request->get("date_to")));
$von = $r->request->get("einsatz_von");
$bis = $r->request->get("einsatz_bis");
$i = 0;
foreach($von as $tag){
$einsatztag = new Einsatztag();
$einsatztag->setNum($i);
$einsatztag->setVon($von[$i]);
$einsatztag->setBis($bis[$i]);
$buchung->addEinsatztage($einsatztag);
$i++;
}
$em = $this->getDoctrine()->getManager();
$em->persist($buchung);
foreach($buchung->getEinsatztage() as $e){
$em->persist($e);
}
$em->flush();
Firstly, you have to understand that Doctrine and Symfony does not work with id's within your entities.In Einsatztag entity, your property should not be called $Buchung_id since it's an instance of buchung and not an id you will find out there.
Moreover, in your loop, you add the Einsatztag to Buchung. But do you process the reverse set ?
I do it this way to always reverse the set/add of entities.
Einsatztag
public function setBuchung(Buchung $pBuchung, $recurs = true){
$this->buchung = $pBuchung;
if($recurs){
$buchung->addEinsatztag($this, false);
}
}
Buchung
public function addEinsatztag(Einsatztag $pEinsatztag, $recurs = true){
$this->einsatztages[] = $pEinsatztag;
if($recurs){
$pEinsatztag->setBuchung($this, false);
}
}
Then, when you will call
$buchung->addEinsatztag($einsatztag);
Or
$einsatztag->set($buchung);
The relation will be set on both side making your FK to be set. Take care of this, you'll have some behavior like double entries if you do not use them properly.
SImplier , you can use default getter/setters and call them on both sides of your relation, using what you already have, like following:
$einsatztag->set($buchung);
$buchung->addEinsatztag($einsatztag);
Hope it helped ;)
First of all, don't use _id properties in your code. Let it be $buchung. If you want it in the database, do it in the annotation. And this also the reason, why it's not working. Your are mapping to buchung, but your property is $Buchung_id
<?php
/** #ORM\Entity **/
class Buchung
{
// ...
/**
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung")
**/
private $einsatztage;
// ...
}
/** #ORM\Entity **/
class Einsatztag
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="einsatztage")
* #JoinColumn(name="buchung_id", referencedColumnName="id")
**/
private $buchung;
// ...
}
You don't have to write the #JoinColumn, because <propertyname>_id would the default column name.
I'm going to ignore the naming issue and add a fix to the actual problem.
You need to have in the adder method a call to set the owner.
//Buchung entity
public function addEinsatztage($einsatztag)
{
$this->einsatztags->add($einsatztag);
$ein->setBuchung($this);
}
And to have this adder called when the form is submitted you need to add to the form collection field the by_reference property set to false.
Here is the documentation:
Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.
http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference
I wrote a vcard class with Phalcon in PHP. The vCard Model is initialized like this.
// Inside the BS_VCard class
public function initialize(){
$this->hasMany("id","BS_VCardElement","vCardId",array(
"alias" => "elements",
'foreignKey' => array(
'action' => Phalcon\Mvc\Model\Relation::ACTION_CASCADE
)
));
}
Its elements are initialized like this
// Inside the BS_VCardElement class
public function initialize(){
$this->belongsTo("vCardId","BS_VCard","id",array("alias" => "vCard"));
...
}
If a user reads a vCard and adds another element, it doesn't work as expected. To simplify the use I added some fascade methods like this
public function addDateOfBirth($date){
$element = new BS_VCardElement();
$element->setName("BDAY");
$element->addValue($date);
// This doesn't work
$this->elements[] = $element;
}
The Docs/Storing related records do not explain how to append fresh data like this to the related table.
I also tried this
$this->elements[] = array_merge($this->elements,array($element));
But the save method seems to ignore the added element. Save() returns true.
This question has been asked a couple of months ago but since I ran into a similar issue I decided to share my results anyway.
And here's what I found. Lower case aliases ('elements') don't seem to work whereas upper case aliases ('Elements') do.
To add one element you can do this;
$this->Elements = $element;
To add multiple elements you can do this;
$elements = array($element1, $element2);
$this->Elements = $elements;
After that you have to save the vcard before accessing the elements again. If you don't, phalcon will just return a result set with only the elements already in the database. (Not sure if this can be changed somehow.)
And here's the documentation (where all this is not mentioned): http://docs.phalconphp.com/en/latest/reference/models.html#storing-related-records
According to the Phalcon source code, the Resultset object is immutible.
/**
* Resultsets cannot be changed. It has only been implemented to
* meet the definition of the ArrayAccess interface
*
* #param int index
* #param \Phalcon\Mvc\ModelInterface value
*/
public function offsetSet(var index, var value)
{
throw new Exception("Cursor is an immutable ArrayAccess object");
}
It appears that replacing the element with an array is the only way to implement an "append" or modification of the resultset (other than delete which IS supported).
Of course this breaks the \Phalcon\Mvc\Model::_preSaveRelatedRecords() because the function ignores the class properties and refetches the related from the Model Manager (and resets the model::$element attribute at the end).
I feel frustrated by this because appending objects to a collection seems like a very common task and not having a clear method in which to add new items to a parent seems like a design flaw.
I think related elements might have some magic functionality invoked when you set the properties, so simply using $this->elements[] (evidently) doesn't work. Perhaps try re-setting the entire variable:
public function addDateOfBirth($date){
$element = new BS_VCardElement();
$element->setName("BDAY");
$element->addValue($date);
$elements = $this->elements;
$elements[] = $element;
$this->elements = $elements;
}
I have an model with a relation, and I want to instantiate a new object of the relations type.
Example: A person has a company, and I have a person-object: now I
want to create a company-object.
The class of the companyobject is defined in the relation, so I don't think I should need to 'know' that class, but I should be able to ask the person-object to provide me with a new instance of type company? But I don't know how.
This is -I think- the same question as New model object through an association , but I'm using PHPActiveRecord, and not the ruby one.
Reason behind this: I have an abstract superclass person, and two children have their own relation with a type of company object. I need to be able to instantiate the correct class in the abstract person.
A workaround is to get it directly from the static $has_one array:
$class = $this::$has_one[1]['class_name'];
$company = new $class;
the hardcoded number can of course be eliminated by searching for the association-name in the array, but that's still quite ugly.
If there is anyone who knows how this is implemented in Ruby, and how the phpactiverecord implementation differs, I might get some Ideas from there?
Some testing has revealed that although the "search my classname in an array" looks kinda weird, it does not have any impact on performance, and in use it is functional enough.
You can also use build_association() in the relationship classes.
Simplest way to use it is through the Model's __call, i.e. if your relation is something like $person->company, then you could instantiate the company with $company = $person->build_company()
Note that this will NOT also make the "connection" between your objects ($person->company will not be set).
Alternatively, instead of build_company(), you can use create_company(), which will save a new record and link it to $person
In PHPActiveRecord, you have access to the relations array. The relation should have a name an you NEED TO KNOW THE NAME OF THE RELATIONSHIP/ASSOCIATION YOU WANT. It doesn't need to be the classname, but the classname of the Model you're relating to should be explicitly indicated in the relation. Just a basic example without error checking or gritty relationship db details like linking table or foreign key column name:
class Person extends ActiveRecord\Model {
static $belongs_to = array(
array('company',
'class_name' => 'SomeCompanyClass')
);
//general function get a classname from a relationship
public static function getClassNameFromRelationship($relationshipName)
foreach(self::$belongs_to as $relationship){
//the first element in all relationships is it's name
if($relationship[0] == $relationshipName){
$className = null;
if(isset($relationship['class_name'])){
$className = $relationship['class_name'];
}else{
// if no classname specified explicitly,
// assume the clasename is the relationship name
// with first letter capitalized
$className = ucfirst($relationship);
}
return $className
}
}
return null;
}
}
To with this function, if you have a person object and want an object defined by the 'company' relationship use:
$className = $person::getClassNameFromRelationship('company');
$company = new $className();
I'm currently using below solution. It's an actual solution, instead
of the $has_one[1] hack I mentioned in the question. If there is a
method in phpactiverecord I'm going to feel very silly exposing
msyelf. But please, prove me silly so I don't need to use this
solution :D
I am silly. Below functionality is implemented by the create_associationname call, as answered by #Bogdan_D
Two functions are added. You should probably add them in the \ActiveRecord\Model class. In my case there is a class between our classes and that model that contains extra functionality like this, so I put it there.
These are the 2 functions:
public function findClassByAssociation($associationName)
Called with the name of the association you are looking for.
Checks three static vars (has_many,belongs_to and has_one) for the association
calls findClassFromArray if an association is found.
from the person/company example: $person->findClassByAssociation('company');
private function findClassFromArray($associationName,$associationArray)
Just a worker-function that tries to match the name.
Source:
/**
* Find the classname of an explicitly defined
* association (has_one, has_many, belongs_to).
* Unsure if this works for standard associations
* without specific mention of the class_name, but I suppose it doesn't!
* #todo Check if works without an explicitly set 'class_name', if not: is this even possible (namespacing?)
* #todo Support for 'through' associations.
* #param String $associationName the association you want to find the class for
* #return mixed String|false if an association is found, return the class name (with namespace!), else return false
* #see findClassFromArray
*/
public function findClassByAssociation($associationName){
//$class = $this::$has_one[1]['class_name'];
$that = get_called_class();
if(isset($that::$has_many)){
$cl = $this->findClassFromArray($associationName,$that::$has_many);
if($cl){return $cl;}
}
if(isset($that::$belongs_to)){
$cl = $this->findClassFromArray($associationName,$that::$belongs_to);
if($cl){return $cl;}
}
if(isset($that::$has_one)){
$cl = $this->findClassFromArray($associationName,$that::$has_one);
if($cl){return $cl;}
}
return false;
}
/**
* Find a class in a php-activerecord "association-array". It probably should have a specifically defined class name!
* #todo check if works without explicitly set 'class_name', and if not find it like standard
* #param String $associationName
* #param Array[] $associationArray phpactiverecord array with associations (like has_many)
* #return mixed String|false if an association is found, return the class name, else return false
* #see findClassFromArray
*/
private function findClassFromArray($associationName,$associationArray){
if(is_array($associationArray)){
foreach($associationArray as $association){
if($association['0'] === $associationName){
return $association['class_name'];
}
}
}
return false;
}