SortController.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
12 
20 
21 /**
22  * SortController is used to change the order of nodes. Nodes can either be
23  * sorted in a list of nodes of the same type (_moveBefore_ action) or in a list
24  * of child nodes of a container node (_insertBefore_ action).
25  *
26  * The controller supports the following actions:
27  *
28  * <div class="controller-action">
29  * <div> __Action__ moveBefore </div>
30  * <div>
31  * Insert an object before a reference object in the list of all objects of the same type.
32  * | Parameter | Description
33  * |------------------------|-------------------------
34  * | _in_ `insertOid` | The object id of the object to insert/move
35  * | _in_ `referenceOid` | The object id of the object to insert the inserted object before. If the inserted object should be the last in the container order, the _referenceOid_ contains the special value `ORDER_BOTTOM`
36  * | __Response Actions__ | |
37  * | `ok` | In all cases
38  * </div>
39  * </div>
40  *
41  * <div class="controller-action">
42  * <div> __Action__ insertBefore </div>
43  * <div>
44  * Insert an object before a reference object in the order of a container object.
45  * | Parameter | Description
46  * |------------------------|-------------------------
47  * | _in_ `containerOid` | The oid of the container object
48  * | _in_ `insertOid` | The oid of the object to insert/move
49  * | _in_ `referenceOid` | The object id of the object to insert the inserted object before. If the inserted object should be the last in the container order, the _referenceOid_ contains the special value `ORDER_BOTTOM`
50  * | _in_ `role` | The role, that the inserted object should have in the container object.
51  * | __Response Actions__ | |
52  * | `ok` | In all cases
53  * </div>
54  * </div>
55  *
56  * @author ingo herwig <ingo@wemove.com>
57  */
58 class SortController extends Controller {
59 
60  const ORDER_BOTTOM = 'ORDER_BOTTOM';
61  const UNBOUND = 'UNBOUND';
62 
63  /**
64  * @see Controller::validate()
65  */
66  protected function validate() {
67 
68  $request = $this->getRequest();
69  $response = $this->getResponse();
70  $persistenceFacade = $this->getPersistenceFacade();
71 
72  $isOrderBottom = $this->isOrderBotton($request);
73 
74  // check object id validity
75  $insertOid = ObjectId::parse($request->getValue('insertOid'));
76  if (!$insertOid) {
77  $response->addError(ApplicationError::get('OID_INVALID',
78  array('invalidOids' => array($request->getValue('insertOid')))));
79  return false;
80  }
81  $referenceOid = ObjectId::parse($request->getValue('referenceOid'));
82  if (!$referenceOid && !$isOrderBottom) {
83  $response->addError(ApplicationError::get('OID_INVALID',
84  array('invalidOids' => array($request->getValue('referenceOid')))));
85  return false;
86  }
87 
88  // action specific
89 
90  if ($request->getAction() == 'moveBefore') {
91  // check matching classes for move operation
92  if (!$isOrderBottom && $insertOid->getType() != $referenceOid->getType()) {
93  $response->addError(ApplicationError::get('CLASSES_DO_NOT_MATCH'));
94  return false;
95  }
96  // check if the class supports order
97  $mapper = $persistenceFacade->getMapper($insertOid->getType());
98  if (!$mapper->isSortable()) {
99  $response->addError(ApplicationError::get('ORDER_UNDEFINED'));
100  return false;
101  }
102  }
103 
104  if ($request->getAction() == 'insertBefore') {
105  // check object id validity
106  $containerOid = ObjectId::parse($request->getValue('containerOid'));
107  if(!$containerOid) {
108  $response->addError(ApplicationError::get('OID_INVALID',
109  array('invalidOids' => array($request->getValue('containerOid')))));
110  return false;
111  }
112 
113  // check association for insert operation
114  $mapper = $persistenceFacade->getMapper($containerOid->getType());
115  $relationDesc = $mapper->getRelation($request->getValue('role'));
116  if (!$relationDesc) {
117  $response->addError(ApplicationError::get('ROLE_INVALID'));
118  return false;
119  }
120  // check if object supports order
121  $otherMapper = $relationDesc->getOtherMapper();
122  if (!$otherMapper->isSortable($relationDesc->getThisRole())) {
123  $response->addError(ApplicationError::get('ORDER_NOT_SUPPORTED'));
124  return false;
125  }
126  }
127  return true;
128  }
129 
130  /**
131  * @see Controller::doExecute()
132  */
133  protected function doExecute() {
134  $request = $this->getRequest();
135  $response = $this->getResponse();
136  $transaction = $this->getPersistenceFacade()->getTransaction();
137 
138  // do actions
139  $transaction->begin();
140  if ($request->getAction() == 'moveBefore') {
141  $this->doMoveBefore();
142  }
143  else if ($request->getAction() == 'insertBefore') {
144  $this->doInsertBefore();
145  }
146  $transaction->commit();
147 
148  $response->setAction('ok');
149  }
150 
151  /**
152  * Execute the moveBefore action
153  */
154  protected function doMoveBefore() {
155  $request = $this->getRequest();
156  $persistenceFacade = $this->getPersistenceFacade();
157  $isOrderBottom = $this->isOrderBotton($request);
158 
159  // load the moved object and the reference object
160  $insertOid = ObjectId::parse($request->getValue('insertOid'));
161  $referenceOid = ObjectId::parse($request->getValue('referenceOid'));
162  $insertObject = $persistenceFacade->load($insertOid);
163  $referenceObject = $isOrderBottom ? new NullNode() : $persistenceFacade->load($referenceOid);
164 
165  // check object existence
166  $objectMap = array('insertOid' => $insertObject,
167  'referenceOid' => $referenceObject);
168  if ($this->checkObjects($objectMap)) {
169  // determine the sort key
170  $mapper = $insertObject->getMapper();
171  $sortkeyDef = $mapper->getSortkey();
172  $sortkey = $sortkeyDef['sortFieldName'];
173 
174  // determine the sort boundaries
175  $referenceValue = $isOrderBottom ? self::UNBOUND : $this->getSortkeyValue($referenceObject, $sortkey);
176  $insertValue = $this->getSortkeyValue($insertObject, $sortkey);
177 
178  // determine the sort direction
179  $isSortup = false;
180  if ($referenceValue == self::UNBOUND || $referenceValue > $insertValue) {
181  $isSortup = true;
182  }
183 
184  // load the objects in the sortkey range
185  $objects = array();
186  $type = $insertObject->getType();
187  if ($isSortup) {
188  $objects = $this->loadObjectsInSortkeyRange($type, $sortkey, $insertValue, $referenceValue);
189  }
190  else {
191  $objects = $this->loadObjectsInSortkeyRange($type, $sortkey, $referenceValue, $insertValue);
192  }
193 
194  // add insert (and reference) object at the correct end of the list
195  if ($isSortup) {
196  $objects[] = $insertObject;
197  // sortkey of reference object does not change
198  // update sort keys
199  $count=sizeof($objects);
200  $lastValue = $this->getSortkeyValue($objects[$count-1], $sortkey);
201  for ($i=$count-1; $i>0; $i--) {
202  $objects[$i]->setValue($sortkey, $this->getSortkeyValue($objects[$i-1], $sortkey));
203  }
204  $objects[0]->setValue($sortkey, $lastValue);
205  }
206  else {
207  array_unshift($objects, $referenceObject);
208  array_unshift($objects, $insertObject);
209  // update sort keys
210  $count=sizeof($objects);
211  $firstValue = $this->getSortkeyValue($objects[0], $sortkey);
212  for ($i=0; $i<$count-1; $i++) {
213  $objects[$i]->setValue($sortkey, $this->getSortkeyValue($objects[$i+1], $sortkey));
214  }
215  $objects[$count-1]->setValue($sortkey, $firstValue);
216  }
217  }
218  }
219 
220  /**
221  * Execute the insertBefore action
222  */
223  protected function doInsertBefore() {
224  $request = $this->getRequest();
225  $persistenceFacade = $this->getPersistenceFacade();
226  $isOrderBottom = $this->isOrderBotton($request);
227 
228  // load the moved object, the reference object and the conainer object
229  $insertOid = ObjectId::parse($request->getValue('insertOid'));
230  $referenceOid = ObjectId::parse($request->getValue('referenceOid'));
231  $containerOid = ObjectId::parse($request->getValue('containerOid'));
232  $insertObject = $persistenceFacade->load($insertOid);
233  $referenceObject = $isOrderBottom ? new NullNode() : $persistenceFacade->load($referenceOid);
234  $containerObject = $persistenceFacade->load($containerOid, 1);
235 
236  // check object existence
237  $objectMap = array('insertOid' => $insertObject,
238  'referenceOid' => $referenceObject,
239  'containerOid' => $containerObject);
240  if ($this->checkObjects($objectMap)) {
241  $role = $request->getValue('role');
242  // add the new node to the container, if it is not yet
243  $nodeExists = sizeof($containerObject->getChildrenEx($insertOid)) == 1;
244  if (!$nodeExists) {
245  $containerObject->addNode($insertObject, $role);
246  }
247  // reorder the children list
248  $children = $containerObject->getChildrenEx(null, $role);
249  $newChildren = array();
250  foreach ($children as $curChild) {
251  $oid = $curChild->getOID();
252  if ($oid == $referenceOid) {
253  $newChildren[] = $insertObject;
254  }
255  if ($oid != $insertOid) {
256  $newChildren[] = $curChild;
257  }
258  }
259  if ($isOrderBottom) {
260  $newChildren[] = $insertObject;
261  }
262  $containerObject->setNodeOrder($newChildren);
263  }
264  }
265 
266  /**
267  * Load all objects between two sortkey values
268  * @param $type The type of objects
269  * @param $sortkeyName The name of the sortkey attribute
270  * @param $lowerValue The lower value of the sortkey or UNBOUND
271  * @param $upperValue The upper value of the sortkey or UNBOUND
272  */
273  protected function loadObjectsInSortkeyRange($type, $sortkeyName, $lowerValue, $upperValue) {
274  $query = new ObjectQuery($type);
275  $tpl1 = $query->getObjectTemplate($type);
276  $tpl2 = $query->getObjectTemplate($type);
277  if ($lowerValue != self::UNBOUND) {
278  $tpl1->setValue($sortkeyName, Criteria::asValue('>', $lowerValue), true);
279  }
280  if ($upperValue != self::UNBOUND) {
281  $tpl2->setValue($sortkeyName, Criteria::asValue('<', $upperValue), true);
282  }
283  $objects = $query->execute(BuildDepth::SINGLE);
284  return $objects;
285  }
286 
287  /**
288  * Check if all objects in the given array are not null and add
289  * an OID_INVALID error to the response, if at least one is
290  * @param $objectMap An associative array with the controller parameter names
291  * as keys and the objects to check as values
292  * @return Boolean
293  */
294  protected function checkObjects($objectMap) {
295  $response = $this->getResponse();
296  $invalidOids = array();
297  foreach ($objectMap as $parameterName => $object) {
298  if ($object == null) {
299  $invalidOids[] = $parameterName;
300  }
301  }
302  if (sizeof($invalidOids) > 0) {
303  $response->addError(ApplicationError::get('OID_INVALID',
304  array('invalidOids' => $invalidOids)));
305  return false;
306  }
307  return true;
308  }
309 
310  /**
311  * Check if the node should be moved to the bottom of the list
312  * @param $request The request
313  * @return Boolean
314  */
315  protected function isOrderBotton($request) {
316  return ($request->getValue('referenceOid') == self::ORDER_BOTTOM);
317  }
318 
319  /**
320  * Get the sortkey value of an object. Defaults to the object's id, if
321  * the value is null
322  * @param $object The object
323  * @param $valueName The name of the sortkey attribute
324  * @return String
325  */
326  protected function getSortkeyValue($object, $valueName) {
327  $value = $object->getValue($valueName);
328  if ($value == null) {
329  $value = $object->getOID()->getFirstId();
330  }
331  return $value;
332  }
333 }
334 ?>
getRequest()
Get the Request instance.
Definition: Controller.php:190
Controller is the base class of all controllers.
Definition: Controller.php:48
SortController is used to change the order of nodes.
doMoveBefore()
Execute the moveBefore action.
ObjectQuery implements a template based object query.
loadObjectsInSortkeyRange($type, $sortkeyName, $lowerValue, $upperValue)
Load all objects between two sortkey values.
static asValue($operator, $value)
Factory method for constructing a Critera that may be used as value on a PersistentObject's attribute...
Definition: Criteria.php:58
NullNode is an implementation of the NullObject pattern, It inherits all functionality from Node (act...
Definition: NullNode.php:27
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:144
doInsertBefore()
Execute the insertBefore action.
checkObjects($objectMap)
Check if all objects in the given array are not null and add an OID_INVALID error to the response...
Application controllers.
Definition: namespaces.php:3
isOrderBotton($request)
Check if the node should be moved to the bottom of the list.
static get($code, $data=null)
Factory method for retrieving a predefind error instance.
getSortkeyValue($object, $valueName)
Get the sortkey value of an object.
getResponse()
Get the Response instance.
Definition: Controller.php:198
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:222