Symfony 2 / Doctrine 2 : get changes to PersistentCollection - php

I am building an application where the user can edit some data and then gets presented with a screen where he can confirm (and comment on) his edits.
In the confirmation form I display the changes that have been made to the entity. This works for "normal" fields. Here is some code that works for checking a single field:
// create $form
// bind $form
if ($form->isValid() {
$data = $form->getData();
// example, get changes of a "normal" field
if ($data['color'] != $entity->getColor()) {
// do something with changes
}
}
But I can't do the same for a relation (example ManyToMany with Users) :
if ($data['users'] != $entity->getUsers()
doesn't work because $data['users'] and $entity->getUsers() refer to the same persistent collection. It is possible to call this function to see if there are changes:
if ($data['users']->isDirty())
but it isn't possible to see what changes were made.
The second problem with the above is that if all items are removed from the persistent collection, Doctrine does not mark it as "changed" (isDirty() = true), so I can't catch the specific change where the user removes all "users" from the entity in the form.
Please note that the code all works, the only problem I have is that I am unable to view/process the changes made on the confirmation step.

Doctrine\ORM\PersistentCollection has internal API (public) methods getSnapshot, getDeleteDiff, getInsertDiff that can be used during lifecycle events of the Doctrine\ORM\UnitOfWork. You could for example check the insert diff of a persistent collection during onFlush.

Solved it like this:
1) To get changes that will be made directly to the Entity, use the following:
// create form
// bind form
// form isValid()
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
$changeset = $uow->getEntityChangeSet($entity);
print_r($changeset);
2a) To get changes to the relations, use the answer from Lighthart above:
$oldUsers = $entity->getUsers()->toArray();
// bind form
// form isValid
$newUsers = $entity->getUsers()->toArray();
// compare $oldUsers and $newUsers
2b) Use these methods on Persistent Collection to find inserts / deletes:
$newUsers = $entity->getUsers();
$inserted = $newUsers->getDeleteDiff();
$deleted = $newUsers->getInsertDiff();
The only problem with (2b) is that if ALL users are removed and none added then getDeleteDiff() is empty which appears to be a Doctrine bug/idiosyncrasy

Store the original collection in a variable before bind and then compared the new collection after bind. PHP has quite a few array comparison functions, and collections are readily turned into native arrays by $collection->toArray();
eg:
// create form
$oldusers=$entity->getUsers()->toArray();
// bind form
if ($form->isValid() {
$data = $form->getData();
if ($data['users'] != $oldusers) {
// do something with changes
}
}

Related

On a Laravel 'update' function, how to parse a JSON object, and store it with a ForEach loop

I am using the Laravel framework to work with my MySQL database, and currently want to update my database from a JSON object, that will be sent from somewhere else.
Currently, I have it the same as my 'Store' function, which is obviously not going to work, because it will update everything, or refuse to work because it is missing information.
This is the for each I have currently, it does not work, but I am not experienced with how it is best to parse a JSON with a for-each, then store it.
public function update(Request $request,$student)
{
$storeData = User::find($student);
foreach ($request as $value) {
$storeData-> username = $value;
}
Here is my store function, with all the info that the front-end team may send in a JSON format.
$storeData->username=$request->input('username');
$storeData->password=$request->input('password');
$storeData->email=$request->input('email');
$storeData->location=$request->input('location');
$storeData->role=DB::table('users')->where('user_id', $student)->value('role');
$storeData->devotional_id=$request->input('devotional_id');
$storeData->gift_id=$request->input('gift_id');
$storeData->save();
return dd("Info Recieved");
You can write the method like the below snippet.
Also, assume you are working with laravel API, so you don't need to parse the incoming JSON input, but you will receive these values as items in the request object.
However, you should use the filled method in order to determine if the field is existing and has a value, the update function will override with empty values otherwise.
I just added this method to the first input, but you have to use it each and every input if you are not sure what the front end will pass.
public function update(Request $request, $student)
{
$storeData = User::find($student); // should be id
if ($request->filled('username')) { // use this for other items also
$storeData->username = $request->input('username');
}
$storeData->password = $request->input('password');
$storeData->email = $request->input('email');
$storeData->location = $request->input('location');
$storeData->role = DB::table('users')->where('user_id', $student)->value('role');
$storeData->devotional_id = $request->input('devotional_id');
$storeData->gift_id = $request->input('gift_id');
$storeData->update();
dd("Info Recieved");
}
Why would they send json data from the front in a post?
Really it would be from a form input. Like Rinto said it would be request object.
$user->username = $request->user_name;
I'm gathering this is a form on the front to create a new user. Why not use the built in auth scaffolding that has this set up for you in the register area?
So I'd personally use...
//look up user that matches the email or create a new user
$user = User::firstOrNew(['email' => request('email')]);
//add other input values here
$user->name = request('name');
//save
$user->save();
Hard to give an exact answer to this when the question is a bit vague in what you're doing. There are many methods in Laravel to accomplish things. From your code it just looks like registration. Also, the big gotcha I see in your code is you are passing a text only password and then adding that password in plain text to your database. That is a big security flaw.
you can convert your JSON object to an array and then do your foreach loop on the new array. To update a table in Laravel it's update ($storeData->update();) not save. Save is to insert.
$Arr = json_decode($request, true);

PHP/Propel delete record 1:n

I've got two tables: step and links joined 1:n. I'm aiming to maintain the links through the step objects. I retrieve all steps from the database and populate the relation with the links table. I persist the step object containing a collection of links to JSON and return it to the front end using REST.
That means that if a step is linked or unlinked to another step in the front end I send the entire step back to the backend including a collection of links. In the back end I use the following code:
public function put($processStep) {
if (isset($processStep['Processesid']) && isset($processStep['Coordx']) && isset($processStep['Coordy'])) {
$p = $this->query->findPK($processStep['Id']);
$p->setId($processStep['Id']);
$p->setProcessesid($processStep['Processesid']);
if (isset($processStep['Flowid'])) $p->setFlowid($processStep['Flowid']);
if (isset($processStep['Applicationid'])) $p->setApplicationid($processStep['Applicationid']);
$p->setCoordx($processStep['Coordx']);
$p->setCoordy($processStep['Coordy']);
$links = $p->getLinksRelatedByFromstep();
$links->clear();
foreach ($processStep['Links'] as $link) {
if (!isset($link['Linkid'])) {
$newLink = new \Link();
$newLink->setFromstep($link['Fromstep']);
$newLink->setTostep($link['Tostep']);
$links->prepend($newLink);
}
}
$p->save();
return $p;
} else {
throw new Exceptions\ProcessStepException("Missing mandatory fields.", 1);
}
}
I'm basically deleting every link from a step and based upon the request object I recreate the links. This saves me the effort to compare what links are deleted and added. The insert work like a charm Propel automatically creates the new links. Thing is it doesn't delete like it inserts. I've checked the object that is being persisted ($p) and I see the link being deleted but in the MySQL log there is absolutely no action being performed by Propel. It looks like a missing member from the link collection doesn't trigger a dirty flag or something like that.
Maybe I'm going about this the wrong way, I hope someone can offer some advice.
Thanks
To delete records, you absolutely always have to use delete. The diff method on the collection is extremely helpful when determining which entities need added, updated, and deleted.
Thanks to Ben I got on the right track, an explicit call for a delete is not needed. I came across a function called: setRelatedBy(ObjectCollection o) I use this function to provide a list of related objects, new objects are interpreted as inserts and omissions are interpreted as deletes.
I didn't find any relevant documentation regarding the problem so here's my code:
$p = $this->query->findPK($processStep['Id']);
$p->setId($processStep['Id']);
$p->setProcessesid($processStep['Processesid']);
$p->setCoordx($processStep['Coordx']);
$p->setCoordy($processStep['Coordy']);
if (isset($processStep['Flowid'])) $p->setFlowid($processStep['Flowid']);
if (isset($processStep['Applicationid'])) $p->setApplicationid($processStep['Applicationid']);
//Get related records, same as populaterelation
$currentLinks = $p->getLinksRelatedByFromstep();
$links = new \Propel\Runtime\Collection\ObjectCollection();
//Check for still existing links add to new collection if so.
//This is because creating a new Link instance and setting columns marks the object as dirty creating an exception due to duplicate keys
foreach ($currentLinks as $currentLink) {
foreach ($processStep['Links'] as $link) {
if (isset($link['Linkid']) && $currentLink->getLinkid() == $link['Linkid']) {
$links->prepend($currentLink);
break;
}
}
}
//Add new link objects
foreach ($processStep['Links'] as $link) {
if (!isset($link['Linkid'])) {
$newLink = new \Link();
$newLink->setFromstep($link['Fromstep']);
$newLink->setTostep($link['Tostep']);
$links->prepend($newLink);
}
}
//Replace the collection and save the processstep.
$p->setLinksRelatedByFromstep($links);
$p->save();

Creating a new Laravel 4 model class with a string variable

TL;DR is at the end to cut to the chase.
I have a lot of belongsTo() models that can have any number of records, and I'm trying to bind them to an edit form. I have the following foreach that creates the form elements:
#foreach ($department->department_10 as $key => $value)
{{ Form::select(
'department_10['.(isset($value->pk_department_10)?$value->pk_department_10:0).']',
$department_10_opts,
(isset($value->department_10)?$value->department_10:''),
array('class'=>'form-control input-md department_10', 'placeholder'=>'Other Types of Service')) }}
#endforeach
Since there can be 0 records (rows?) that belong to the model, to simplify my #foreach, I wanted to create a "blank" instance of the model. Additionally, because I'm going to have to deal with about 70 more cases like this, I created a function that would create the new blank model. Here's the function (in my controller for lack of a better place):
function mkBlankModel($parentModel, $newModel){
if(count($parentModel->$newModel) === 0){
$parentModel->$newModel[0] = new $newModel();
$parentModel->$newModel[0]->fk_department = $parentModel->pk_department;
$parentModel->$newModel[0]->$newModel = '';
}
return $parentModel;
}
When I run it, I don't get any errors, but I do get unexpected results and I can't really make sense of them:
Test Step 1) View the edit page while loading a record with 2 department_10's. It works as expected; loads two fields properly.
Test Step 2) View the edit page while loading a record with 0 department_10's. The page loads but without any fields. Because apparently my function didn't work, so I verify by dumping dump($department->$department_10) and it confirms this.
Test Step 3) I replace the $parentModel->$newModel[0] with $parentModel->department_10[0] like so:
function mkBlankModel($parentModel, $newModel){
if(count($parentModel->$newModel) === 0){
$parentModel->department_10[0] = new $newModel();
$parentModel->department_10[0]->fk_department = $parentModel->pk_department;
$parentModel->department_10[0]->$newModel = '';
}
return $parentModel;
}
And both scenarios (with records and without) work just fine. So my problem likely isn't Laravel specific, but I'm just curious how I can accomplish this.
TL;DR:
I'm trying to create a model instance for a parent model, if one doesn't exist, so a blank field will be created by my #foreach loop in my edit.blade.php's form. I can do this just fine if I manually spell out the model's name when creating it, but since I'll be doing this frequently, I'd prefer to define the class, and populate it with a string.
So I figured it out; I needed to wrap the $newModel in curly-brackets:
function mkBlankModel($parentModel, $newModel){
if(count($parentModel->$newModel) === 0){
$parentModel->{$newModel}[0] = new $newModel();
$parentModel->{$newModel}[0]->fk_department = $parentModel->pk_department;
$parentModel->{$newModel}[0]->$newModel = '';
}
return $parentModel;
}
More info here: http://php.net/manual/en/language.types.string.php#language.types.string.parsing.complex

Yii on update, detect if a specific AR property has been changed on beforeSave()

I am raising a Yii event on beforeSave of the model, which should only be fired if a specific property of the model is changed.
The only way I can think of how to do this at the moment is by creating a new AR object and querying the DB for the old model using the current PK, but this is not very well optimized.
Here's what I have right now (note that my table doesn't have a PK, that's why I query by all attributes, besides the one I am comparing against - hence the unset function):
public function beforeSave()
{
if(!$this->isNewRecord){ // only when a record is modified
$newAttributes = $this->attributes;
unset($newAttributes['level']);
$oldModel = self::model()->findByAttributes($newAttributes);
if($oldModel->level != $this->level)
// Raising event here
}
return parent::beforeSave();
}
Is there a better approach? Maybe storing the old properties in a new local property in afterFind()?
You need to store the old attributes in a local property in the AR class so that you can compare the current attributes to those old ones at any time.
Step 1. Add a new property to the AR class:
// Stores old attributes on afterFind() so we can compare
// against them before/after save
protected $oldAttributes;
Step 2. Override Yii's afterFind() and store the original attributes immediately after they are retrieved.
public function afterFind(){
$this->oldAttributes = $this->attributes;
return parent::afterFind();
}
Step 3. Compare the old and new attributes in beforeSave/afterSave or anywhere else you like inside the AR class. In the example below we are checking if the property called 'level' is changed.
public function beforeSave()
{
if(isset($this->oldAttributes['level']) && $this->level != $this->oldAttributes['level']){
// The attribute is changed. Do something here...
}
return parent::beforeSave();
}
Just in one line
$changedArray = array_diff_assoc($this->attributes,
$this->oldAttributes);
foreach($changedArray as $key => $value){
//What ever you want
//For attribute use $key
//For value use $value
}
In your case you want to use if($key=='level') inside of foreach
Yii 1.1: mod-active-record at yiiframework.com
or Yii Active Record instance with "ifModified then ..." logic and dependencies clearing at gist.github.com
You can store old properties with hidden fields inside update form instead of loading model again.

Filtering form input within Symfony form class

I would like to filter some fields in my form with strtolower() function. Unfortunately I can't find any example of doing that.
How can I write such filter, that will lowercase the input, check the database if element exists and then decide wheter to add the record or not?
1) new project custom validator (we will use it like value filter here):
/lib/validator/MyProjectStringLowerCase.class.php
<?php
class MyProjectStringLowerCase extends sfValidatorBase
{
/**
* #see sfValidatorBase
*/
protected function doClean($value)
{
return strtolower($value);
}
}
2) bound it to field:
$this->setWidget('my_field_name', new sfWidgetFormInputText());
$this->validatorSchema['my_field_name'] = new MyProjectStringLowerCase();
If you have some validator on that field already, you can merge them into combined validators this way:
$this->validatorSchema['my_field_name'] = new sfValidatorAnd(array(
$this->validatorSchema['my_field_name'], // the original field validator
new MyProjectStringLowerCase(),
));
The combined validators order influent how value will flow trough them, so if you want to have value filtrated in second validation, set MyProjectStringLowerCase as the first one.
There are 2 differences between this approach and using post processing (like doSave() for instance):
the value here will be filtered after each send (and will show
filtered in displaying of form errors)
You can reuse it very cleanly and easily in other fields or forms in
your project
In your Form, you can override the doSave() method to do any manual interventions that you need to do that aren't completed by the form validation methods.
For example:
public function doSave($con = null) {
$employee = $this->getObject();
$values = $this->getValues();
// do your filter
$this->values['name'] = strtolower($values['name']);
parent::doSave($con);
}

Categories