Silverstripe 3 - Detect changes on many_many relation - silverstripe

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.

Related

Adding type to a document uploaded in Alfresco through behaviour

I am trying to associate a document type to xz:xylo, whenever a document is uploaded in a particular workspace of Alfresco, it should get attached to a type which I defined in xylomodel.xml.
I am trying to achieve this via Alfresco behaviour as procceding via Share has some limitation for my requirement.
Can anyone please correct me if the code attached is syntactically correct and I am approaching correctly.
enter code here
public class ApplyXyloAspect implements NodeServicePolicies.OnCreateNodePolicy {`
private NodeService nodeService;
private PolicyComponent policyComponent;
// Behaviours
private Behaviour onCreateNode;
}
/**
^When a document of type #XyloCmsType(name = "X:xz:Xylo") is created than aspects from xyloModel.xml
^needs to be applied
*/
public void init() {
// Create behaviours
if workspace=workspace://SpacesStore/973e1b8d-bf61-8196-3278-fbbf0b4375gg
org.alfresco.repo.node.NodeServicePolicies this.onCreateNode = new JavaBehaviour(this, "onCreateNode", NotificationFrequency.FIRST_EVENT);
// Bind behaviours to node policies
this.policyComponent.bindClassBehaviour(Qname.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"),
Qname.createQName(XYLO.NAMESPACE_XYLO_CONTENT_MODEL, XYLO.TYPE_xz_xyloModel),
this.onCreateNode
);
}
Depending on your requirements you might be better off achieving this through Folder Rules.
If folder rules are not adequate, or if I'm misunderstanding your use of the very specific NodeRef of workspace://SpacesStore/973e1b8d-bf61-8196-3278-fbbf0b4375gg then I would just check in the onCreateNode method if the created node's parent matches that NodeRef, rather than trying to check in the init method.
so in your init method you would just do something like this:
this.onCreateNode = new JavaBehaviour(this, "onCreateNode", Behaviour.NotificationFrequency.FIRST_EVENT);
this.policyComponent.bindClassBehaviour(NodeServicePolicies.OnCreateNodePolicy.QNAME, Qname.createQName(XYLO.NAMESPACE_XYLO_CONTENT_MODEL, XYLO.TYPE_xz_xyloModel), this.onCreateNode);
And then just check if the node is a child of the node you're trying to have be the parent, in this case you said it would be workspace://SpacesStore/973e1b8d-bf61-8196-3278-fbbf0b4375gg.
So the onCreateNode method would look something like this.
#Override
public void onCreateNode(ChildAssociationRef childAssociationRef){
NodeRef idealParentNodeRef = new NodeRef("workspace://SpacesStore/973e1b8d-bf61-8196-3278-fbbf0b4375gg");
NodeRef nodeRef = childAssociationRef.getChildRef();
NodeRef parentRef = childAssociationRef.getParentRef();
//First double check and make sure all the nodes exist.
if(nodeService.exists(nodeRef) && nodeService.exists(parentRef) && nodeService.exists(idealParentNodeRef)){
//then check if the parentRef and the idealParentNodeRef match
if(parentRef.equals(idealParentNodeRef)){
nodeService.addAspect(nodeRef, /*QName of the Aspect you want to add*/);
}
}
}
If you know for a fact the node/workspace you're uploading to will be very specific every time you could just do this, though I would probably also suggest throwing in some error handling, logging, etc. but this would get you started at least.
Note that, generally, you shouldn't necessarily expect the NodeRef to stay the same every time, granted, I'm just showing you what you could do based on the information from your post rather than what you should do (which would be finding some other way to reference the NodeRef/workspace you're trying to use, and going on from there, depending on whether that NodeRef/workspace is a Folder or Site, or something else).
Hope this helps.

Inheritance - Sharing info between child and parent controllers

Context
I have a custom Event Entity which has several child Entities: Problem and Maintenance (and few others but those two should be enough to describe the problem) entity classes inherit from Event entity class.
The addAction(), seeAction() and modifyAction() of ProblemController and MaintenanceController are (obviously) very similar but with some differences.
I want to have a button to display the see view of an Event, no matter if it is a Problem or a Maintenance. Same for modify.
For the add action it is a bit different: the user has to say (by clicking on child-specific button) what kind of child he want to add.
How I handle this so far
In my seeAction() and modifyAction(), I just forward the "call" depending on the type of the child:
public function seeAction(Event $event)
{
if($event instanceof \Acme\EventBundle\Entity\Problem){
return $this->forward('AcmeEventBundle:Problem:see', array('event_id' => $event->getId()));
}
elseif($event instanceof \Acme\EventBundle\Entity\Maintenance){
return $this->forward('AcmeEventBundle:Maintenance:see', array('maintenance_id' => $event->getId()));
}
}
I have no Event::addAction() but I have a Event::addCommon() which gathers the common parts of the addAction of Problem and Maintenance. Then I call this Event::addCommon() with Controller inheritance.
class ProblemController extends EventController
{
public function addAction(MeasurementSite $measurementSite)
{
$problem = new Problem();
$problem->setMeasurementSite($measurementSite);
$form = $this->createForm(new ProblemType($measurementSite), $problem);
$response = parent::addCommon($problem, $form);
return $response;
}
Problem
All this looks pretty ugly to me. If I want to share common things between Problem::seeAction() and Maintenance::seeAction(), I will have to call an Event function, but Event already forwarded something!! Information jumps from Parent to Child and vice versa...
I would like to know what is the proper way to manage this problem?
I looked a bit at setting Controller as a service, using PHP Traits, Routing inheritance but I couldn't extract anything clear and clean from this research...
I can see how you might end up chasing your tail on this sort of problem.
Instead of multiple controllers, consider have one EventController for all the routes along with individual ProblemHelper and MaintainenceHelper objects. The helper objects would have your add/see/modify methods and could extend a CommonHelper class.
Your controller would check the entity type, instantiate the helper and pass control over to it.

ZF2: Prefill fieldset data

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(),
);
}

In Drupal 7, why is autocomplete not attaching to this list of form elements?

I'd like to attach autocomplete to a particular list of fields in Drupal 7. The fields have FIELD_CARDINALITY_UNLIMITED, so there could be anywhere from 1 to whatever. I'm using the following code:
function mymodule_form_alter(&$form, &$form_state, $form_id) {
if (array_key_exists('mymodule', $form)) {
$indices = array_filter(
array_keys($form['mymodule']['und']),
function($item) {
return is_numeric($item);
}
);
foreach($indices as $index) {
$form['mymodule']['und'][$index]['value']['#autocomplete_path'] = 'api/node/title';
}
}
}
...however, my autocomplete behavior is not being attached. I've used the exact same code in a similar situation - the only difference is that I was adding the autocomplete to a field that had a cardinality of 1 rather than unlimited. That doesn't seem like it should change anything. I've verified that the autocomplete is attaching by doing a debug($form['mymodule']) after the assignment statement, and it is definitely there. I have also debugged the exact array path I am trying to get in each iteration of the foreach loop, and it is definitely the correct form value.
EDIT: Is it possible that the issue is with more than one module altering this form using hook_form_alter()? I'm performing the exact same operation as above (but on a single field) in a different module, on the same form.
EDIT2: I've noticed that if I put a debug statement inside the foreach loop, I see the autocomplete value is set on the proper value each iteration. If I place the debug statement outside the foreach loop, the autocomplete path is no longer set. Somehow, either during the course of iteration, or after iteration, it looks like my changes are being destroyed? I tested this by assuming $index to be 0, and writing a hard-coded statement to attach autocomplete - this allowed auto complete to work correctly. To be clear, I am seeing something like the following:
function mymodule_form_alter(&$form, &$form_state, $form_id) {
if (array_key_exists('mymodule', $form)) {
$indices = array_filter(
array_keys($form['mymodule']['und']),
function($item) {
return is_numeric($item);
}
);
foreach($indices as $index) {
$form['mymodule']['und'][$index]['value']['#autocomplete_path'] = 'api/node/title';
// Debug statements here show that the value '#autocomplete_path' is set properly
debug($form)['mymodule']['und'][$index]['value']);
}
// Now, the '#autocomplete_path' key does not exist
debug($form)['mymodule']['und'][0]['value']);
// This will make autocomplete attach correctly
$form['mymodule']['und'][0]['value']['#autocomplete_path'] = 'api/node/title';
}
}
You've spelt it #autcomplete_path...it should be #autocomplete_path :)
If you're defining the field (and widget) yourself then you should just add the autocomplete in your module's implementation of hook_field_widget_form() rather than altering the form.
If you're not defining the widget yourself, take a look at hook_field_widget_form_alter() and hook_field_widget_WIDGET_TYPE_form_alter() which will let you alter the widget form for a specific field.
Try this:
1) change ['mymodule']['und'][$index]['value'] in your code to the id of your text form input example
$form['search_form_block']
['#autocomplete_path']='yourcall_back_function_which_returns_data';
I think the mistake is your are trying to work to replace the value of the the field but you have to change the value of the format widget. In this case the input field.
2) Also make sure 'api/node/title' call back works using x debug.
Let me know if it worked.
Cheers,
vishal
I resolved the problem by manually enumerating my indices rather than programmatically doing so, e.g. $form['mymodule']['und'][0]... - this appears to be a PHP issue related to scoping of variables in foreach rather than a Drupal problem.

Showing default widget value only when object value is not set (in NEW mode)

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
}

Resources