In my symfony2/doctrine2 application, I have a weird case where when persisting a modified entity, the change is not flushed to the database, and I can't understand why.
Below is my code :
$date = $subscription->getPaymentValidUntil()->format('d/m/Y');
$period = $payment->getDetail('period');
$validDate = $subscription->getPaymentValidUntil()
->add(new\DateInterval($period == Subscription::MONTH ? 'P1M' : 'P1Y'))
;
$subscription->setPaymentValidUntil($validDate);
$this->em->persist($subscription);
exit(var_dump(array(
$date,
$this->em->getUnitOfWork()->getScheduledEntityUpdates(),
$subscription->getPaymentValidUntil(),
)));
$this->em->flush();
The output of the vardump is the following:
array (size=3)
0 => string '12/05/2015' (length=10)
1 =>
array (size=0)
empty
2 =>
object(DateTime)[2295]
public 'date' => string '2015-06-12 18:52:37' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Paris' (length=12)
If I flush before the vardump or remove the vardump, indeed, the date value does not change in my database. Why?
As you can see, I add one month to this date value, it's reflected in the entity, but it's not scheduled for update. How can I solve this ?
EDIT : I've digged into the entity manager persist function and I figured out that when dumping the entity in Doctrine\ORM\EntityManager::persist the date was good but when dumping it in Doctrine\ORM\UnitOfWork::persist, the date had somehow been reseted to its original one :
Doctrine\ORM\EntityManager::persist : dumps the updated entity
public function persist($entity)
{
if ( ! is_object($entity)) {
throw ORMInvalidArgumentException::invalidObject('EntityManager#persist()' , $entity);
}
$this->errorIfClosed();
if ($entity instanceof Subscription) exit(var_dump($entity));
$this->unitOfWork->persist($entity);
}
Doctrine\ORM\UnitOfWork::persist : dumps the non-modified entity : WHY ?
private function doPersist($entity, array &$visited)
{
if ($entity instanceof Subscription) exit(var_dump($entity));
$oid = spl_object_hash($entity);
if (isset($visited[$oid])) {
return; // Prevent infinite recursion
}
}
DateTime are object after all, and are always passed by reference. So, I strongly suggest you to edit your code like this:
$period = $payment->getDetail('period');
$validDate = clone $subscription->getPaymentValidUntil();
$validDate->add(new \DateInterval($period == Subscription::MONTH ? 'P1M' : 'P1Y'));
$subscription->setPaymentValidUntil($validDate);
$this->em->persist($subscription);
$this->em->flush();
This way, you ensure that you're passing a different object to the entity, and avoid any subtle bug in the EntityManager.
Related
given that I have a class like this one:
class DefaultProductContainer
{
public function __invoke(array $data, array $mandant): Product
{
$date = new \DateTime('now');
$metaData = [
"createdAt" => '',
"updatedAt" => $date->format('Y-m-d H:i:s.u'),
"status" => ['de'],
];
...
return Product::fromArray($params);
}
....
I want to test, if the created object is equal to what I expect.
But of course they differ, because the time they have been created differs.
Now I know from javascript how to test it with Faketimers. See How do I set a mock date in Jest?
Which is quite neat, because it enables to make such code testable, without changing the original code. And has many more advantages, when we are talking about date-calculations.
I searched now for more than two hours. Is there anything similar for PHP?
Intro
So, i have an entity Presence that is watched by Doctrine.
I also have another entity Decision that is not watched by Doctrine.
The Presence entity has an attribute $decisions, typed ArrayCollection and contains 0 to n Decision entities.
Since i don't store each Decision object that is contained within the ArrayCollection, i typed the attribute $decision as Doctrine Array and everything is working so far.
I can get the whole Presence object back from database & it contains every Decision objects that i added to it.
The problem
When i edit one Decision object that is present in the ArrayCollection of my Presence object like :
foreach($Presence->getDecisions() as $decision) {
$decision->setMorning($value);
}
The modifications are applied locally, and i can see them when doind a dump($Presence);
Presence {#2354 ▼
#id: 8
#decisions: ArrayCollection {#1409 ▼
-elements: array:6 [▼
0 => Decision {#1408 ▼
#token_id: "005867b2915438c03383bb831d87c26346c586d6ecd46b735c7a23b4fabb682a8ac34a90ea3d53cc55c2209deaa83337abeb2b98ded43967fcfb0f4d04d5ada6"
#date: DateTime #1531692000 {#1407 ▶}
#morning: -1
#afternoon: 0
}
1 => Decision {#1406 ▶}
2 => Decision {#1404 ▶}
3 => Decision {#1402 ▶}
4 => Decision {#1400 ▶}
5 => Decision {#1398 ▶}
]
}
#last_edit_date: DateTime #1532109183 {#2454 ▼
date: 2018-07-20 19:53:03.062940 Europe/Berlin (+02:00)
}
#user: User {#1393 ▶}
}
On this dump() you can see that the attribute #morning: -1 is the result of the setter call.
But when i try to flush the changes with $this->entityManager->flush(); the ArrayCollection has not been modified.
At first i thought it was a problem of changes detection, so i added a #last_edit_date DateTime field onto my Presence entity.
The fun part is that this attribute is updating just fine when flushing.
It seems that the ArrayCollection won't update for some reason.
UPDATE
To complete my post here is additional informations :
The full method in the controller
/**
* #Route(name="edit_decision", path="/edit-decision/{tokenDate}/{dayPart}/{newValue}")
*/
public function EditDecisionAction(Request $request)
{
$token = $request->get('tokenDate');
$dayPart = $request->get('dayPart');
$newValue = intval($request->get('newValue'));
$myPresence = $this->em->getRepository('AppBundle:Presence')->findOneBy([
'user' => $this->connectedUser
]);
/** #var ArrayCollection $decisionCollection */
$decisionCollection = $myPresence->getDecisions();
$found = FALSE;
/** #var Decision $decision */
foreach ($decisionCollection as $decision) {
if ($decision->getTokenId() == $token) {
$found = TRUE;
if ($dayPart === 'morning') {
$decision->setMorning($newValue);
}elseif ($dayPart === 'afternoon') {
$decision->setAfternoon($newValue);
}
break;
}
}
if (!$found) {
throw $this->createNotFoundException();
}
// I set a new Date to trigger a modification on the Presence Entity : It works
$myPresence->setLastEditDate(new \DateTime());
// Here is the dump that you can see above
dump($myPresence);
$this->em->flush();
try{
$this->em->flush();
return $this->json([
'status' => TRUE
]);
}catch (\Exception $exception) {
return $this->json([
'status' => FALSE
]);
}
}
The attribute that contains the ArrayCollection :
/**
* Doctrine Type Array
*
* #ORM\Column(type="array", nullable=true)
*/
protected $decisions;
Any help or even hints are welcome !
Thanks :)
UPDATE 2
I may have found the solution thanks to this post : How to force Doctrine to update array type fields?
The problem comes from the doctrine Array type under which i store my objects :
Doctrine uses identical operator (===) to compare changes between old and new values. The operator used on the same object (or array of objects) with different data always return true. There is the other way to solve this issue, you can clone an object that needs to be changed.
Update 3
Nope still doesn't persists the changes made on the ArrayCollection's items in the database.
Gonna try Cloning : Adding : Removing the old one and we will se what happens.
FINAL UPDATE
Ok so i finally got it to work using a trick.
The main problem was the behaviour of Doctrine changes detection towards the Array type.
As quoted above, doctrine does : Array === Array to check if there is changes to be made.
Kinda weird because when you ADD or REMOVE an element from your ArrayCollection, for Doctrine the collection is still equivalent to the precedent state.
There should be a way to test equivalences between 2 objects like in Java .equals to remove those ambiguities.
Anyway what i did to solve it :
I store my collection to an initial state :
$decisionCollection = $myPresence->getDecisions();
I place my setters & modify my collection accordingly :
foreach ($myPresence->getDecisions() as $key => $decision) {
if ($decision->getTokenId() == $token) {
if ($dayPart === 'morning') {
$decision->setMorning($newValue);
} elseif ($dayPart === 'afternoon') {
$decision->setAfternoon($newValue);
}
break;
}
}
And then the Trick :
$myPresence->unsetDecisions();
$this->em->flush();
I unset completly my collection ($this->collection = NULL) & flush a first time.
It triggers the Update since Array !== NULL
And then i set my collection with the updated one that contains the setted objects :
$myPresence->setDecisions($decisionCollection);
$myPresence->setLastEditDate(new \DateTime());
And flush a last time to keep the new changes.
Thanks everyone for your time & your comments !
I am using Cakephp 3.0.2. I would like to add a new record. A complete record $this->request->data would look something like this;
'date_taken' => [
'month' => '01',
'day' => '05',
'year' => '2015',
'hour' => '02',
'minute' => '51'
],
'moves_per_minute' => '80',
'person_id' => '1'
However, I want to HTTP Post $this->request->data such that it looks like this;
'moves_per_minute' => '80',
'person_id' => '1'
The dates_taken data should be updated automatically by using the current time.
I defined the column dates_taken as DATETIME type and default as CURRENT_TIME_STAMP. When I do a HTTP Post and send $this->request->data over leaving date_taken empty, the new row cannot be added.
This is how my controller code looks like;
$newRecord = $this->newRecords->newEntity();
if ($this->request->is('post')) {
$newRecord = $this->newRecords->patchEntity($newRecord, $this->request->data);
if ($this->newRecords->save($newRecord))
{
//success
Debugger::dump("Save success");
}
else
{
$this->response->statusCode(400); //bad request
Debugger::dump("Save failure");
}
}
How should a correct controller look like?
Add the columns 'created' and 'modified' to your database table (both DATETIME), then inside the model for the table add this line:
class ArticlesTable extends Table
{
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
// Timestamp is the correct, even though the field is a datetime
}
}
After you add new records, the datetime stored in the created field will be the exact time of creation and the modified field will be the last time you modify the record. Cake will do this by default every time you access this record through the entity.
More information can be found via the Timestamp documentation here: http://book.cakephp.org/3.0/en/orm/behaviors/timestamp.html
You could also save the current time by using the Time library and manually setting it in the entity before saving:
$time = Time::now();
$newRecord->date_taken = $time;
$this->newRecords->save($newRecord);
Again assuming the date_taken field is a datetime object in your table. I'd recommend letting the database set it for you rather than this method, though.
Ok this is a 2 part question, first primary concern is the nextval() bit. I have a table where one of the columns is an auto incrementing column. Being fairly new to postgres and Zend DB I am running into trouble. Inserting a new row on to a table.
This is what i have thus far (and in the CLI this works). But not in the Zend DB stuff..
class Comments extends Zend_Db_Table_Abstract
{
protected $_name = 'comments';
public function setComment($comment_id = null, $id = null, $comment = null)
{
if(($comment_id == null)||(empty($comment_id))||(trim($comment_id) == '')){return false;}
if(($id == null)||(empty($id))||(trim($id) == '')){return false;}
if(($comment == null)||(empty($comment))||(trim($comment) == '')){return false;}
echo 'check one';
$db = Zend_Db_Table::getDefaultAdapter();
if($comment_id == "none")
{
$created = time()*1000;
$data = array(
"comment_id" => nextval('comments_comment_id_seq'),
"comment" => $comment,
"comment_level" => 1,
"is_active" => true,
"status" => 0,
"id" => $id,
"user_id" => $_SESSION['role']['user_id'],
"ts_create" => $created,
"ts_mod" => $created
);
$table->insert($data);
return true;
}
}
}
The Error I am getting is:
Fatal error: Call to undefined function nextval() in
/home/src/branches/portal/application/model/Comments.php
on line 45
So Not really sure how to adjust for that. I am more familiar with mySQL where I could just drop NULL and it would auto increment. So all in all the big problem is the nextval, does Zend DB have anything in the framework that I can fix this issue with?
Next part, since this is a two part question. By using $table->insert($data); is that going to automatically use the $_name variable? I read through the Zend doc on the subject but its confusing me a bit currently as how to define the right table in the case of an insert.
Use this:
"comment_id" => new Zend_Db_Expr("nextval('comments_comment_id_seq')"),
I wanted to specify the output of a field from within my model so I added a date key to my $_schema:
models/Tags.php
<?php
protected $_schema = array(
'id' => array('type' => 'integer', 'key' => 'primary'),
'title' => array('type' => 'string'),
'created' => array('type' => 'integer', 'date' => 'F jS, Y - g:i a'),
'modified' => array('type' => 'integer')
);
?>
I store my time as an unsigned integer in the db (output of time()).
I want my base model to format any field that has the date key for output. I thought the best place to do that would be right after a find:
extensions/data/Model.php
<?php
static::applyFilter('find', function($self, $params, $chain) {
$schema = $self::schema();
$entity = $chain->next($self, $params, $chain);
foreach ($schema as $field => $options) {
if (array_key_exists('date', $options)) {
//format as a date
$params['data'][$field] = $entity->formatDate($field, $options['date']);
}
}
return $entity;
});
public function formatdate($entity, $field, $format, $timezone = 'Asia/Colombo') {
$dt = new \DateTime();
$tz = new \DateTimeZone($timezone);
$dt->setTimestamp($entity->$field);
$dt->setTimezone($tz);
return $dt->format($format);
}
?>
This doesn't seem to be working. When I execute a find all, this filter seems to get hit twice. The first time, $entity contains a count() of the results and only on the second hit does it contain the Records object.
What am I doing wrong? How do I alter this so that simply doing <?= $tag->created; ?> in my view will format the date the way I want? This, essentially, needs to be an 'after filter', of sorts.
EDIT
If I can find a way to access the current model entity object (not the full namespaced path, $self contains that), I can probably solve my problem.
Regardless of a small fix for your after find filter, I would do it differently.
Every time you'll do a find, you'll override your date format, even if you don't want to display it, but only do a business logic like comparing dates etc ...
Since you want to format your output only in your views (we are not talking about formatting on the fly json responses for an API, etc.), why not using a helper method ?
An other way is to add an instance method in your model (or a BaseModel), called created_at(). Then, you will call it from a view with <?= $entity->created_at() ?>
You can still force a format fetched from your $_schema, or pass it as a param, etc ...
A helper seems cleaner as we are talking about presenting data in your views.
I'm reading the OP's problem as the find filter executes twice. If that's right, then why not just check to see if the $entity contains a recordset?