SortController.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 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 
22 
23 /**
24  * SortController is used to change the order of nodes. Nodes can either be
25  * sorted in a list of nodes of the same type (_moveBefore_ action) or in a list
26  * of child nodes of a container node (_insertBefore_ action).
27  *
28  * The controller supports the following actions:
29  *
30  * <div class="controller-action">
31  * <div> __Action__ moveBefore </div>
32  * <div>
33  * Insert an object before a reference object in the list of all objects of the same type.
34  * | Parameter | Description
35  * |------------------------|-------------------------
36  * | _in_ `insertOid` | The object id of the object to insert/move
37  * | _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`
38  * | __Response Actions__ | |
39  * | `ok` | In all cases
40  * </div>
41  * </div>
42  *
43  * <div class="controller-action">
44  * <div> __Action__ insertBefore </div>
45  * <div>
46  * Insert an object before a reference object in the order of a container object.
47  * | Parameter | Description
48  * |------------------------|-------------------------
49  * | _in_ `containerOid` | The oid of the container object
50  * | _in_ `insertOid` | The oid of the object to insert/move
51  * | _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`
52  * | _in_ `role` | The role, that the inserted object should have in the container object.
53  * | __Response Actions__ | |
54  * | `ok` | In all cases
55  * </div>
56  * </div>
57  *
58  * @author ingo herwig <ingo@wemove.com>
59  */
60 class SortController extends Controller {
61 
62  const ORDER_BOTTOM = 'ORDER_BOTTOM';
63  const UNBOUND = 'UNBOUND';
64 
65  /**
66  * @see Controller::validate()
67  */
68  protected function validate() {
69 
70  $request = $this->getRequest();
71  $response = $this->getResponse();
72  $persistenceFacade = $this->getPersistenceFacade();
73 
74  $isOrderBottom = $this->isOrderBotton($request);
75 
76  // check object id validity
77  $insertOid = ObjectId::parse($request->getValue('insertOid'));
78  if (!$insertOid) {
79  $response->addError(ApplicationError::get('OID_INVALID',
80  ['invalidOids' => [$request->getValue('insertOid')]]));
81  return false;
82  }
83  $referenceOid = ObjectId::parse($request->getValue('referenceOid'));
84  if (!$referenceOid && !$isOrderBottom) {
85  $response->addError(ApplicationError::get('OID_INVALID',
86  ['invalidOids' => [$request->getValue('referenceOid')]]));
87  return false;
88  }
89 
90  // action specific
91 
92  if ($request->getAction() == 'moveBefore') {
93  // check matching classes for move operation
94  if (!$isOrderBottom && $insertOid->getType() != $referenceOid->getType()) {
95  $response->addError(ApplicationError::get('CLASSES_DO_NOT_MATCH'));
96  return false;
97  }
98  // check if the class supports order
99  $mapper = $persistenceFacade->getMapper($insertOid->getType());
100  if (!$mapper->isSortable()) {
101  $response->addError(ApplicationError::get('ORDER_UNDEFINED'));
102  return false;
103  }
104  }
105 
106  if ($request->getAction() == 'insertBefore') {
107  // check object id validity
108  $containerOid = ObjectId::parse($request->getValue('containerOid'));
109  if(!$containerOid) {
110  $response->addError(ApplicationError::get('OID_INVALID',
111  ['invalidOids' => [$request->getValue('containerOid')]]));
112  return false;
113  }
114 
115  // check association for insert operation
116  $mapper = $persistenceFacade->getMapper($containerOid->getType());
117  $relationDesc = $mapper->getRelation($request->getValue('role'));
118  if (!$relationDesc) {
119  $response->addError(ApplicationError::get('ROLE_INVALID'));
120  return false;
121  }
122  // check if object supports order
123  $otherMapper = $relationDesc->getOtherMapper();
124  if (!$otherMapper->isSortable($relationDesc->getThisRole())) {
125  $response->addError(ApplicationError::get('ORDER_NOT_SUPPORTED'));
126  return false;
127  }
128  }
129  return true;
130  }
131 
132  /**
133  * @see Controller::doExecute()
134  */
135  protected function doExecute($method=null) {
136  $this->requireTransaction();
137  $request = $this->getRequest();
138  $response = $this->getResponse();
139 
140  // do actions
141  if ($request->getAction() == 'moveBefore') {
142  $this->doMoveBefore();
143  }
144  else if ($request->getAction() == 'insertBefore') {
145  $this->doInsertBefore();
146  }
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 = ['insertOid' => $insertObject,
167  'referenceOid' => $referenceObject];
168  if ($this->checkObjects($objectMap)) {
169  // determine the sort key
170  $mapper = $insertObject->getMapper();
171  $type = $insertObject->getType();
172  $sortkeyDef = $mapper->getSortkey();
173  $sortkey = $sortkeyDef['sortFieldName'];
174  $sortdir = strtoupper($sortkeyDef['sortDirection']);
175 
176  // get the sortkey values of the objects before and after the insert position
177  if ($isOrderBottom) {
178  $lastObject = $this->loadLastObject($type, $sortkey, $sortdir);
179  $prevValue = $lastObject != null ? $this->getSortkeyValue($lastObject, $sortkey) : 1;
180  $nextValue = ceil($sortdir == 'ASC' ? $prevValue+1 : $prevValue-1);
181  }
182  else {
183  $nextValue = $this->getSortkeyValue($referenceObject, $sortkey);
184  $prevObject = $this->loadPreviousObject($type, $sortkey, $nextValue, $sortdir);
185  $prevValue = $prevObject != null ? $this->getSortkeyValue($prevObject, $sortkey) :
186  ceil($sortdir == 'ASC' ? $nextValue-1 : $nextValue+1);
187  }
188 
189  // set the sortkey value to the average
190  $insertObject->setValue($sortkey, ($nextValue+$prevValue)/2);
191  }
192  }
193 
194  /**
195  * Execute the insertBefore action
196  */
197  protected function doInsertBefore() {
198  $request = $this->getRequest();
199  $persistenceFacade = $this->getPersistenceFacade();
200  $isOrderBottom = $this->isOrderBotton($request);
201 
202  // load the moved object, the reference object and the conainer object
203  $insertOid = ObjectId::parse($request->getValue('insertOid'));
204  $referenceOid = ObjectId::parse($request->getValue('referenceOid'));
205  $containerOid = ObjectId::parse($request->getValue('containerOid'));
206  $insertObject = $persistenceFacade->load($insertOid);
207  $referenceObject = $isOrderBottom ? new NullNode() : $persistenceFacade->load($referenceOid);
208  $containerObject = $persistenceFacade->load($containerOid);
209 
210  // check object existence
211  $objectMap = ['insertOid' => $insertObject,
212  'referenceOid' => $referenceObject,
213  'containerOid' => $containerObject];
214  if ($this->checkObjects($objectMap)) {
215  $role = $request->getValue('role');
216  $originalChildren = $containerObject->getValue($role);
217  // add the new node to the container, if it is not yet
218  $nodeExists = sizeof(Node::filter($originalChildren, $insertOid)) == 1;
219  if (!$nodeExists) {
220  $containerObject->addNode($insertObject, $role);
221  }
222  // reorder the children list
223  $orderedChildren = [];
224  foreach ($originalChildren as $curChild) {
225  $oid = $curChild->getOID();
226  if ($oid == $referenceOid) {
227  $orderedChildren[] = $insertObject;
228  }
229  if ($oid != $insertOid) {
230  $orderedChildren[] = $curChild;
231  }
232  }
233 
234  // get the sortkey values of the objects before and after the insert position
235  if ($isOrderBottom) {
236  $orderedChildren[] = $insertObject;
237  }
238  $containerObject->setNodeOrder($orderedChildren, [$insertObject], $role);
239  }
240  }
241 
242  /**
243  * Load the object which order position is before the given sort value
244  * @param $type The type of objects
245  * @param $sortkeyName The name of the sortkey attribute
246  * @param $sortkeyValue The reference sortkey value
247  * @param $sortDirection The sort direction used with the sort key
248  */
249  protected function loadPreviousObject($type, $sortkeyName, $sortkeyValue, $sortDirection) {
250  $query = new ObjectQuery($type);
251  $tpl = $query->getObjectTemplate($type);
252  $tpl->setValue($sortkeyName, Criteria::asValue($sortDirection == 'ASC' ? '<' : '>', $sortkeyValue), true);
253  $pagingInfo = new PagingInfo(1, true);
254  $objects = $query->execute(BuildDepth::SINGLE, [$sortkeyName." ".($sortDirection == 'ASC' ? 'DESC' : 'ASC')], $pagingInfo);
255  return sizeof($objects) > 0 ? $objects[0] : null;
256  }
257 
258  /**
259  * Load the last object regarding the given sort key
260  * @param $type The type of objects
261  * @param $sortkeyName The name of the sortkey attribute
262  * @param $sortDirection The sort direction used with the sort key
263  */
264  protected function loadLastObject($type, $sortkeyName, $sortDirection) {
265  $query = new ObjectQuery($type);
266  $pagingInfo = new PagingInfo(1, true);
267  $invSortDir = $sortDirection == 'ASC' ? 'DESC' : 'ASC';
268  $objects = $query->execute(BuildDepth::SINGLE, [$sortkeyName." ".$invSortDir], $pagingInfo);
269  return sizeof($objects) > 0 ? $objects[0] : null;
270  }
271 
272  /**
273  * Check if all objects in the given array are not null and add
274  * an OID_INVALID error to the response, if at least one is
275  * @param $objectMap An associative array with the controller parameter names
276  * as keys and the objects to check as values
277  * @return Boolean
278  */
279  protected function checkObjects($objectMap) {
280  $response = $this->getResponse();
281  $invalidOids = [];
282  foreach ($objectMap as $parameterName => $object) {
283  if ($object == null) {
284  $invalidOids[] = $parameterName;
285  }
286  }
287  if (sizeof($invalidOids) > 0) {
288  $response->addError(ApplicationError::get('OID_INVALID',
289  ['invalidOids' => $invalidOids]));
290  return false;
291  }
292  return true;
293  }
294 
295  /**
296  * Check if the node should be moved to the bottom of the list
297  * @param $request The request
298  * @return Boolean
299  */
300  protected function isOrderBotton($request) {
301  return ($request->getValue('referenceOid') == self::ORDER_BOTTOM);
302  }
303 
304  /**
305  * Get the sortkey value of an object. Defaults to the object's id, if
306  * the value is null
307  * @param $object The object
308  * @param $valueName The name of the sortkey attribute
309  * @return String
310  */
311  protected function getSortkeyValue($object, $valueName) {
312  $value = $object->getValue($valueName);
313  if ($value == null) {
314  $value = $object->getOID()->getFirstId();
315  }
316  return $value;
317  }
318 }
319 ?>
checkObjects($objectMap)
Check if all objects in the given array are not null and add an OID_INVALID error to the response,...
loadPreviousObject($type, $sortkeyName, $sortkeyValue, $sortDirection)
Load the object which order position is before the given sort value.
doMoveBefore()
Execute the moveBefore action.
isOrderBotton($request)
Check if the node should be moved to the bottom of the list.
loadLastObject($type, $sortkeyName, $sortDirection)
Load the last object regarding the given sort key.
SortController is used to change the order of nodes.
static asValue($operator, $value)
Factory method for constructing a Criteria that may be used as value on a PersistentObject's attribut...
Definition: Criteria.php:58
Criteria defines a condition on a PersistentObject's attribute used to select specific instances.
Definition: Criteria.php:21
getSortkeyValue($object, $valueName)
Get the sortkey value of an object.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:28
BuildDepth values are used to define the depth when loading object trees.
Definition: BuildDepth.php:19
requireTransaction()
Start or join a transaction that will be committed at the end of execution.
Definition: Controller.php:334
ApplicationError is used to signal errors that occur while processing a request.
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:135
static get($code, $data=null)
Factory method for retrieving a predefined error instance.
NullNode is an implementation of the NullObject pattern, It inherits all functionality from Node (act...
Definition: NullNode.php:26
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:283
getRequest()
Get the Request instance.
Definition: Controller.php:251
static filter(array $nodeList, ObjectId $oid=null, $type=null, $values=null, $properties=null, $useRegExp=true)
Get Nodes that match given conditions from a list.
Definition: Node.php:182
Node adds the concept of relations to PersistentObject.
Definition: Node.php:34
Application controllers.
Definition: namespaces.php:3
Controller is the base class of all controllers.
Definition: Controller.php:49
PagingInfo contains information about a paged list.
Definition: PagingInfo.php:18
getResponse()
Get the Response instance.
Definition: Controller.php:259
doInsertBefore()
Execute the insertBefore action.
ObjectQuery implements a template based object query.