I have an issue with Symfony 2.6 form collections. Removing elements from collection works, however only when at least one is present. If the last element is removed from DOM (leaving the collection container empty), no elements are removed from the collection after handling request.
Example:
I have a form with collection "children" and two children, "a" and "b". I remove the child "b" from DOM, save, removeChild is called, the child is removed. Now I also remove child "a", save, nothing happens - after refreshing the form the child is still present.
When dumping the main entity after the form has handled request, the child is present in it's collection as well.
Did anyone have similar problem and found a solution?
Thanks to #Daniel pointing me in a new direction I have found a solution. The method submit does in fact accept second argument - clear empty. Passing request to submit is however deprecated and will be removed in Symfony 3.0.
Handle request does in fact support the clear empty feature.
It is simply not passed manually, but based on the request method. If the method is post, clear empty is set to true. If method is patch, clear empty is false. In my case the method was patch, hence the issue.
You can do this by 2 methods :
as in symfony doc : form collection doc
or with doctrine annotation : doctrine orphanRemoval doc
But don't forget cascade={"remove"} annotation
You can delete all the items of a Collection of an Entity simply by:
$request['collectionName'] = null; //or empty array
$form->submit($request, false);
Problems arise when this $request comes from a javascript Ajax call:
var item = {field: 'test', collectionName: null};
ajaxPatchRequest(item);
since the null value is received as String "null". If collectionName is an empty array it will not be passed within the ajax call.
Thus, use the null value and apply a preprocessing before $form->submit():
$toPatch = array();
foreach($request->request->all() as $key => $value) {
if($value === 'null') {
$toPatch[$key] = null;
} else {
$toPatch[$key] = $value;
}
}
$form->submit($toPatch, false);
Related
I have an entity called foo which has an OneToMany association with an entity called bar that is accessible as $foo->getBar() (an ArrayCollection). Normally calling $foo->getBar() would trigger a Lazy Loading of associated bar entities (if they weren't joined originally).
How can I check if bar has been loaded without triggering a Lazy Load? I don't need the associated entities, if they weren't loaded originally, and I don't want them to load, I just want to know IF they were loaded.
Example
In the fooRepository I have a method called getFooWithBar() and that has a join which loads all the bars as an ArrayCollection and returns foo with all the associated bar entities. But if I just call a simpler method like getFooById() with a simple query, the bar entities were not loaded with a join, so they are not contained in $foo.
So in another controller I have $foo, and I want to check if getBar() has associated entities loaded yet, but I do not want to trigger the Lazy Loading. If it doesn't have associated entities, I don't want them. I just need to know IF they have been loaded.
NOTE: I also do not want to turn off Lazy Loading on the entity association for all instances.
Method that Doesn't Work for Inverse side of OneToMany
I put this magic getter method in my entity:
public function __get($property) {
return isset($this->$property) ? $this->$property : null;
}
Which theoretically lets me check if the property is set (or if it's still the default private declaration). And this works when my entity is the owning side. But if it's the inverse side, $this->property is never set. Doctrine does some fancy stuff so that when you do getProperty() it's looking at the data somewhere else. I figured this out because this function works when it's the owning side (it returns a proxy of the associated entity), but it returns null when the associated entity is owned by the other entity.
After years of testing our code (responding to Doctrine changes) the following is the best solution we could come up with to check if an association has been loaded, WITHOUT trigger LazyLoad. None of this stuff is documented in Doctrine (unfortunately), so you have to look at the source code and/or play with the code.
The Solution
In the end there are many different types of different associations that could be loaded from *ToMany (PersistentCollection) or *ToOne associations (Proxy or direct entity). This means we need to create a method that checks for all the possibilities (that we are currently aware of in our app). We created a trait that we add to all our entities, so we can call $entity->isLoaded($propertyName) to check if it's loaded.
public function isLoaded($property)
{
// *ToMany Association are PersistentCollection and will have the isInitialized property as true if it's loaded
if ($this->{$property} instanceof PersistentCollection) {
return $this->{$property}->isInitialized();
}
// *ToOne Associations are (sometimes) Proxy and will be marked as __isInitialized() when they are loaded
if ($this->{$property} instanceof Proxy) {
return $this->{$property}->__isInitialized();
}
// NOTE: Doctrine Associations will not be ArrayCollections. And they don't implement isInitalized so we really
// can tell with certainty whether it's initialized or loaded. But if you join entities manually and want to check
// you will need to set an internal mapper that records when you've loaded them. You could return true if count > 0
if ($this->{$property} instanceof ArrayCollection) {
// NOTE: __isLoaded[$property] is an internal property we record on the Setter of special properties we know are ArrayCollections
return (!empty($this->__isLoaded[$property]) || $this->{$property}->count() > 0);
}
// NOTE: there are never any Collections that aren't ArrayCollection or PersistentCollection (and it does no good to check because they won't have isInitialized() on them anyway
// If it's an object after the checks above, we know it's not NULL and thus it is "probably" loaded because we know it's not a Proxy, PersistentCollection or ArrayCollection
if (is_object($this->{$property})) {
return true;
}
// If it's not null, return true, otherwise false. A null regular property could return false, but it's not an Entity or Collection so indeed it is not loaded.
return !is_null($this->{$property});
}
When you load your foo object, bar will be an instance of Doctrine\ORM\PersistentCollection. You can call the isInitialized() method on this collection to find out if has been initialized.
For Associations that are an ArrayCollection:
$initialized = $foo->getBar()->isInitialized();
If you have newest version of Doctrine, you can try extra lazy load on column.
More Extra lazy associations
I've got dataobjects called attribute and attribute set
The attribute set has this many_many relation to attribute
private static $many_many = array(
'Attributes' => 'Attribute'
);
on attribute I've got this
private static $belongs_many_many = array(
'Sets' => 'AttributeSet'
);
You can add attributes to an set either directly from the set or on the attribute.
Now I need to know when a new attribute is added to a set, to update another content afterwards. I tried it with
public function onBeforeWrite(){
parent::onBeforeWrite();
if( $this->isChanged('Attributes') ){
$this->Title = 'test';
}
}
on the attribute set, but like presumed, it doesn't work, because the set get's not written if a new attribute is added.
Is there a way to do this?
Thank you in advance
You can serialize in some way like json_encode the ManyManyList and store it in a private variable during the init stage, then you can deserialize it during the onBeforeWrite and check for differences.
It's not an efficient task, but I think it's the only way you have to achieve your goal.
Couldn't you do something like this?
public function onBeforeWrite(){
parent::onBeforeWrite();
foreach($this->Attributes() as $attribute) {
if($attribute->isChanged()) {
$this->Title = 'test';
break;
}
}
}
Update: I now realise that this will not work for objects that are deleted. Maybe it is an option to do things the other way around. So do an onBeforeDelete on the many_many objects that sets the field in the "parent(s)" and then saves it. You could even do this for onbeforeWrite as well...
update 2:
It is a little unclear what you want. Do you want to know if the many_many objects have changed, regardless of when this happens, or do you just want to know if they change during the current page load?
isChanged only works when you load the object from the database, and then change something during the same cycle. The remainder of the current execution cycle, isChanged will return true. The next cycle, the object is reloaded, and isChanged returns back to false.
If you want to know if something changed since the last time you opened the parent object, you should store it in the database itself, or in the parent object (also in the db). This is quite easy, by just changing the parent object(s) with a boolean flag, and then saving again. If you want to track changes you need to implement something like #g4b0 suggests, or maybe try to add versioning to your objects. But the latter would probably force you to do a lot of custom coding.
I've got an issue with my fieldset in Zend Framework 2.
The user is able to save his personal data over a form. If he already save this, they should be prefilled with the data from database. This worked fine as it was only a form, but I need the address data in a fieldset, so that I can use it at other parts of my program. Now the input fields stays empty.
At the beginning, I fill the personal data in a session. My data looks like this:
object(Application\Model\Product\PersonalData)#247 (3) {
["tel":protected]=> string(0) ""
["birthday":protected]=> string(10) "2013-01-01"
["address":protected]=> object(Application\Model\Account\Addresses)#248 (15) {
["firstname":protected]=> string(5) "Ernie"
["surname":protected]=> string(6) "Muppet"
...
}
}
As you can see, the data is already bind to the given objects, PersonalData as main, and Addresses for the fieldset. This seems to work then.
Then I put it in my form:
$oForm->setData($oForm->getHydrator()->extract($_SESSION->getPersonalData()));
return new ViewModel(array('form'=>$oForm));
The addressFieldset has a hydrator and a binding, which does work, because all objects are perfectly filled. The only problem is, that when I open the page, the input-fields are empty, only birthday and telephone are filled, which are directly on the form
My form implements the address-fieldset like this:
$addressFieldset = new AddressFieldset($lang);
$addressFieldset->setUseAsBaseFieldset(true);
$addressFieldset->setName('address');
$this->add($addressFieldset);
I think that it might be just a problem with the correct addressing of my fieldset, but I can't explain why it would be filled correctly after posting the data then. All I want is that he fill the setData in my Fieldset.
I hope you understand my question and could help me.
Thanks a lot,
Svenja
EDIT:
I analysed it a bit more now, it's very strange and I don't know what went wrong.
When I debug I can see that $fieldset->getValue() returns all data I need. So I thought that the binding might be wrong and I did this to debug it step by step:
$values = $this->form->get('address')->getValue();
$addressFieldset = $this->form->get('address');
$aValues = $addressFieldset->getHydrator()->extract($values);
$addressFieldset->bindValues($aValues);
I went to the Fieldset.php and bindValues does perfectly what it should (it is only a recapitulation anyway(?)), call the hydrator and fill my object. But if I show in the elements, all values are NULL.
I already checked my elements twice. The only different between the model and the elements is a different order of declaration. I call the method setUseAsBaseField(true) in the fieldset and the form, too. I can't understand why the data is in values but not in elements.
It's very strange, because I have something and it good work. Are you confident that the expression $_SESSION->getPersonalData() to return the desired result? You are using a very strange session.
http://framework.zend.com/manual/2.1/en/modules/zend.session.container.html
To EDIT (I'm sorry my english):
You can use different types of hydrator, for example
ArraySerializable (by default, your entity must have getArrayCopy()
and exchangeArray() methods, in your case) ArraySerializable is the hydrator by default.
First you have to bind a form with entity $form->bind(new Entity()); The entity will bind to the base fieldset. If the base fieldset not specified, the entity will bind with the form, because the form inherits fieldset. If the entity is an aggregate, ie, its properties contain another objects, for each of these objects should be your fildset.
In constructor this children fieldsets you should use
$this->setObject(new MyChildrenEntity());
As a result, the entity properties will be extracted to the form
elements.
After that, you should only work with the form, but not with its
elements or fieldsets.
You can pass any data in the form, so form elements will get this
values. $form->setData($this->getRequest()->getPost()); This method use internal populateValues() method. If the form has elements with an appropriate name, they will be assigned to these values. If the form has fieldsets, they will also be transferred to these values.
As long as the form fails to validate, the entity of these values
will not be assigned. These values are assigned to entities only
in case of successful verification. IsValid () method uses the
internal method bindValues () if the validation was successful.
After successful validation, you can get the entity using getData()
method $entity = $form->getData();
P.S.: If you are doing a complex "haсk", do not be offended by this simple explanation.
I finally found the solution!
At first, this link helped me:
Populating fieldsets
I didn't integrate the fieldset Input-Filter in the Form Input Filter.
But that wasn't all I have to do. My Form-Datamodel looks like this:
interface IPersonalData
{
public function getTel();
public function getBirthday();
public function getAddress();
public function setTel($tel);
public function setAddress(IAddresses $address);
public function setBirthday($birthday);
}
This is only the interface, but Address is an object. And that is the problem. When the form tries to fill the fieldset, he only accept arrays. So I have to extract my object in the getter-method to an array.
I don't think that this is very useful, because I normaly want to get my object with this method. So I write a method "getAddressAsArray()" which looks like this:
public function getAddressAsArray()
{
$oAddressHydrator = new AddressHydrator();
if(isset($this->address))
{
return $oAddressHydrator->extract($this->address);
}
return array();
}
The extract-method of my hydrator changed like this:
public function extract($object)
{
if(!$object instanceof IPersonalData)
{
throw new \InvalidArgumentException('$object must be an instance of Application\Model\Product\IPersonalData');
}
return array(
'telephone' => $object->getTel(),
'address' => $object->getAddressAsArray(),
'birthday' => $object->getBirthday(),
);
}
So I got this in my routing.yml:
requirements:
var1: \d+
var2: \d+
Both are checked on their own and valid.
I need to check the combination of the 2, since the combination is not always valid.
For this case I need to check the relation between 2 objects in the database, the first should be the parent of the second.
I can do this in the controller, but I don't really like that implementation. Also, I need this same check for more than 1 route.
How would I add another requirement that checks the combination? Can I define a method in the controller class that would be called?
Or would the best solution be something like:
public function indexAction($var1, $var2)
{
$result = $this->checkRelation($var1, $var2);
if ($result) {
// return errorpage
return $result;
}
// ...
}
So as I understand your question, you want the following:
/parent/child/ --> returns 200
/not_parent/not_child --> returns 404
The Symfony2 Routing component doesn't do this natively, but you could extend it.
http://symfony.com/doc/master/cmf/cookbook/using-a-custom-route-repository.html
The final solution I went with was the following:
add a method checkRelation that requires all parameters
run a query inside that method to check if everything is ok.
return false when there is a problem, return true when values are ok. (alternatively you can return an object or something)
in the action I check if the value is false, if so I return a generic "not found" page for the specific controller.
In all this is very similar to what I posted in my initial question.
When using the same checkRelation in multiple controllers it might be a good idea to move it (partially) to a repository-class or something similar to prevent duplication of code/logic.
THIS QUESTION IS NOT ABOUT HOW TO SET DEFAULT VALUE OF A WIDGET
Hello Symfonians!
I had a fundamental doubt about forms, Im putting 2 scenarios below.
I have a customModelForm that extends a modelForm.
1> If I do not specify a default value for a form field
new: field is empty
edit: field shows the value in the object
2> If I specify a default value for a field,
new: field shows default value
edit: field shows default value
I am trying to avoid the EDIT mode behaviour in scenario 2.
The default value should only be displayed when the value in the object is not set.
I am calling parent::configure after setting the default value. Do we have any control on the 'bind' event?
Thanks
This shouldn't be happening, at least in Doctrine. The part of the code where this is happening is in updateDefaultsFromObject in sfFormDoctrine. The relevant lines are:
if ($this->isNew())
{
$defaults = $defaults + $this->getObject()->toArray(false);
}
else
{
$defaults = $this->getObject()->toArray(false) + $defaults;
}
updateDefaultsFromObject does net get called until the entire configure chain is done, so something else must be going on here.
Are you using Doctrine? Are you using the most current version of Symfony (there was a bug here a while ago)? Are you sure the default is getting set in the configure method of your form?
The isNew check richsage is recommending should be avoided. There is a larger issue here as the proper behavior is for default value to get overwritten by an existing object's values.
First of all, call parent::configure() first in your configure() method. That way you don't run the risk of your configuration being overwritten by the parent configuration.
You can set defaults based on the model's status by doing something like the following in your configure() method:
if ($this->getObject()->isNew())
{
// do something here but only if the object is new
}
else
{
// the object is being edited
}