AbstractMapper.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 
32 
33 /**
34  * AbstractMapper provides a basic implementation for other mapper classes.
35  * It handles authorization on entity level and calls the lifecycle callbacks
36  * of PersistentObject instances. Authorization on attribute level has to be
37  * implemented by subclasses.
38  *
39  * @author ingo herwig <ingo@wemove.com>
40  */
41 abstract class AbstractMapper implements PersistenceMapper {
42 
43  private $logStrategy = null;
44 
45  private $attributes = [];
46  private $relations = [];
47 
48  private static $logger = null;
49 
50  protected $persistenceFacade = null;
51  protected $permissionManager = null;
52  protected $concurrencyManager = null;
53  protected $eventManager = null;
54 
55  /**
56  * Constructor
57  * @param $persistenceFacade
58  * @param $permissionManager
59  * @param $concurrencyManager
60  * @param $eventManager
61  */
66  if (self::$logger == null) {
67  self::$logger = LogManager::getLogger(__CLASS__);
68  }
69  $this->persistenceFacade = $persistenceFacade;
70  $this->permissionManager = $permissionManager;
71  $this->concurrencyManager = $concurrencyManager;
72  $this->eventManager = $eventManager;
73  }
74 
75  /**
76  * Set the OutputStrategy used for logging persistence actions.
77  * @param $logStrategy
78  */
79  public function setLogStrategy(OutputStrategy $logStrategy) {
80  $this->logStrategy = $logStrategy;
81  }
82 
83  /**
84  * @see PersistenceMapper::getTypeDisplayName()
85  */
86  public function getTypeDisplayName(Message $message) {
87  return $message->getText($this->getType());
88  }
89 
90  /**
91  * @see PersistenceMapper::getTypeDescription()
92  */
93  public function getTypeDescription(Message $message) {
94  return $message->getText("");
95  }
96 
97  /**
98  * @see PersistenceMapper::hasRelation()
99  */
100  public function hasRelation($roleName) {
101  $this->initRelations();
102  return isset($this->relations['byrole'][$roleName]);
103  }
104 
105  /**
106  * @see PersistenceMapper::getRelation()
107  */
108  public function getRelation($roleName) {
109  if ($this->hasRelation($roleName)) {
110  return $this->relations['byrole'][$roleName];
111  }
112  throw new PersistenceException("No relation to '".$roleName."' exists in '".$this->getType()."'");
113  }
114 
115  /**
116  * @see PersistenceMapper::getRelationsByType()
117  */
118  public function getRelationsByType($type) {
119  $this->initRelations();
120  if (isset($this->relations['bytype'][$type])) {
121  return $this->relations['bytype'][$type];
122  }
123  throw new PersistenceException("No relation to '".$type."' exists in '".$this->getType()."'");
124  }
125 
126  /**
127  * @see PersistenceMapper::hasAttribute()
128  */
129  public function hasAttribute($name) {
130  $this->initAttributes();
131  return isset($this->attributes['byname'][$name]);
132  }
133 
134  /**
135  * @see PersistenceMapper::getAttribute()
136  */
137  public function getAttribute($name) {
138  if ($this->hasAttribute($name)) {
139  return $this->attributes['byname'][$name];
140  }
141  throw new PersistenceException("No attribute '".$name."' exists in '".$this->getType()."'");
142  }
143 
144  /**
145  * @see PersistenceMapper::getReferences()
146  */
147  public function getReferences() {
148  $this->initAttributes();
149  return $this->attributes['refs'];
150  }
151 
152  /**
153  * @see PersistenceMapper::getAttributeDisplayName()
154  */
155  public function getAttributeDisplayName($name, Message $message) {
156  return $message->getText($name);
157  }
158 
159  /**
160  * @see PersistenceMapper::getAttributeDescription()
161  */
162  public function getAttributeDescription($name, Message $message) {
163  return $message->getText("");
164  }
165 
166  /**
167  * @see PersistenceMapper::getProperties()
168  */
169  public function getProperties() {
170  return [];
171  }
172 
173  /**
174  * @see PersistenceMapper::load()
175  */
176  public function load(ObjectId $oid, $buildDepth=BuildDepth::SINGLE) {
177  if (!$this->checkAuthorization($oid, PersistenceAction::READ)) {
179  return;
180  }
181 
182  if (!ObjectId::isValid($oid) || $oid->containsDummyIds()) {
183  return null;
184  }
185  // load object
186  $object = $this->loadImpl($oid, $buildDepth);
187  if ($object != null) {
188  // call lifecycle callback
189  $object->afterLoad();
190  }
191  return $object;
192  }
193 
194  /**
195  * @see PersistenceMapper::create()
196  */
197  public function create($type, $buildDepth=BuildDepth::SINGLE) {
198  // Don't check rights here, because object creation may be needed
199  // for internal purposes. That newly created objects may not be saved
200  // to the storage unless they are valid and the user is authorized
201  // is assured by the save method.
202  $object = $this->createImpl($type, $buildDepth);
203 
204  // call lifecycle callback
205  $object->afterCreate();
206 
207  return $object;
208  }
209 
210  /**
211  * @see PersistenceMapper::save()
212  */
213  public function save(PersistentObject $object) {
214  $isDirty = ($object->getState() == PersistentObject::STATE_DIRTY);
215  $isNew = ($object->getState() == PersistentObject::STATE_NEW);
216 
217  $oid = $object->getOID();
218  if ($isDirty) {
219  // check permissions for changed attributes, because this
220  // also includes instance and type checks
221  $oidStr = $object->getOID()->__toString();
222  foreach ($object->getChangedValues() as $valueName) {
223  $resource = $oidStr.'.'.$valueName;
224  if (!$this->checkAuthorization($resource, PersistenceAction::UPDATE)) {
226  return;
227  }
228  }
229  }
230  elseif ($isNew && !$this->checkAuthorization($oid, PersistenceAction::CREATE)) {
232  return;
233  }
234 
235  // call lifecycle callback
236  if ($isDirty) {
237  $object->beforeUpdate();
238  }
239  elseif ($isNew) {
240  $object->beforeInsert();
241  }
242 
243  // validate object
244  $object->validateValues();
245 
246  // check concurrency
247  $this->concurrencyManager->checkPersist($object);
248 
249  // save object
250  $this->saveImpl($object);
251 
252  // update lock
253  $this->concurrencyManager->updateLock($oid, $object);
254 
255  // call lifecycle callback
256  if ($isDirty) {
257  $object->afterUpdate();
258  $this->eventManager->dispatch(PersistenceEvent::NAME,
259  new PersistenceEvent($object, PersistenceAction::UPDATE, $oid));
260  }
261  elseif ($isNew) {
262  $object->afterInsert();
263  $this->eventManager->dispatch(PersistenceEvent::NAME,
264  new PersistenceEvent($object, PersistenceAction::CREATE, $oid));
265  }
266  }
267 
268  /**
269  * @see PersistenceMapper::delete()
270  */
271  public function delete(PersistentObject $object) {
272  $oid = $object->getOID();
273  if (!$this->checkAuthorization($oid, PersistenceAction::DELETE)) {
275  return;
276  }
277 
278  if (!ObjectId::isValid($oid)) {
279  return false;
280  }
281  // call lifecycle callback
282  $object->beforeDelete();
283 
284  // check concurrency
285  $this->concurrencyManager->checkPersist($object);
286 
287  // delete object
288  $result = $this->deleteImpl($object);
289  if ($result === true) {
290  // call lifecycle callback
291  $object->afterDelete();
292  $this->eventManager->dispatch(PersistenceEvent::NAME,
293  new PersistenceEvent($object, PersistenceAction::DELETE, $oid));
294 
295  // release any locks on the object
296  $this->concurrencyManager->releaseLocks($oid);
297  }
298  return $result;
299  }
300 
301  /**
302  * @see PersistenceMapper::getOIDs()
303  */
304  public function getOIDs($type, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null) {
305  $oids = $this->getOIDsImpl($type, $criteria, $orderby, $pagingInfo);
306  $tx = $this->persistenceFacade->getTransaction();
307 
308  // remove oids for which the user is not authorized
309  $result = [];
310  for ($i=0, $count=sizeof($oids); $i<$count; $i++) {
311  $oid = $oids[$i];
312  if ($this->checkAuthorization($oid, PersistenceAction::READ)) {
313  $result[] = $oid;
314  }
315  else {
316  $tx->detach($oid);
317  }
318  }
319  return $result;
320  }
321 
322  /**
323  * @see PersistenceMapper::loadObjects()
324  */
325  public function loadObjects($type, $buildDepth=BuildDepth::SINGLE, $criteria=null, $orderby=null,
326  PagingInfo $pagingInfo=null) {
327  $objects = $this->loadObjectsImpl($type, $buildDepth, $criteria, $orderby, $pagingInfo);
328  $tx = $this->persistenceFacade->getTransaction();
329 
330  // remove objects for which the user is not authorized
331  $result = [];
332  for ($i=0, $count=sizeof($objects); $i<$count; $i++) {
333  $object = $objects[$i];
334  if ($this->checkAuthorization($object->getOID(), PersistenceAction::READ)) {
335  // call lifecycle callback
336  $object->afterLoad();
337  $result[] = $object;
338  }
339  else {
340  $tx->detach($object->getOID());
341  }
342  // TODO remove attribute values for which the user is not authorized
343  // should use some pre-check if restrictions on the entity type exist
344  }
345  return $result;
346  }
347 
348  /**
349  * @see PersistenceMapper::loadRelation()
350  */
351  public function loadRelation(array $objects, $role, $buildDepth=BuildDepth::SINGLE, $criteria=null, $orderby=null,
352  PagingInfo $pagingInfo=null) {
353  $relatedObjects = $this->loadRelationImpl($objects, $role, $buildDepth, $criteria, $orderby, $pagingInfo);
354  $tx = $this->persistenceFacade->getTransaction();
355 
356  // remove objects for which the user is not authorized
357  if ($relatedObjects != null) {
358  $result = [];
359  foreach ($relatedObjects as $oidStr => $curObjects) {
360  $curResult = [];
361  for ($i=0, $count=sizeof($curObjects); $i<$count; $i++) {
362  $object = $curObjects[$i];
363  if ($this->checkAuthorization($object->getOID(), PersistenceAction::READ)) {
364  $curResult[] = $object;
365  }
366  else {
367  $tx->detach($object->getOID());
368  }
369  }
370  $result[$oidStr] = $curObjects;
371  }
372  return $result;
373  }
374  return null;
375  }
376 
377  /**
378  * Log the state of the given object
379  * @param $obj PersistentObject instance
380  */
381  protected function logAction(PersistentObject $obj) {
382  if ($this->logStrategy) {
383  $this->logStrategy->writeObject($obj);
384  }
385  }
386 
387  /**
388  * Check authorization on a resource (type/instance/instance property) and a given action.
389  * @param $resource Resource to authorize
390  * @param $action Action to authorize
391  * @return Boolean depending on success of authorization
392  */
393  protected function checkAuthorization($resource, $action) {
394  return $this->permissionManager->authorize($resource, '', $action);
395  }
396 
397  /**
398  * Handle an authorization error.
399  * @param $resource
400  * @param $action
401  * @throws AuthorizationException
402  */
403  protected function authorizationFailedError($resource, $action) {
404  // when reading only log the error to avoid errors on the display
405  $msg = ObjectFactory::getInstance('message')->
406  getText("Authorization failed for action '%0%' on '%1%'.", [$action, $resource]);
407  if ($action == PersistenceAction::READ) {
408  self::$logger->error($msg."\n".ErrorHandler::getStackTrace());
409  }
410  else {
411  throw new AuthorizationException($msg);
412  }
413  }
414 
415  /**
416  * Initialize relations.
417  */
418  private function initRelations() {
419  if ($this->relations == null) {
420  $this->relations = [];
421  $this->relations['byrole'] = [];
422  $this->relations['bytype'] = [];
423 
424  $relations = $this->getRelations();
425  foreach ($relations as $relation) {
426  $this->relations['byrole'][$relation->getOtherRole()] = $relation;
427  $otherType = $relation->getOtherType();
428  if (!isset($this->relations['bytype'][$otherType])) {
429  $this->relations['bytype'][$otherType] = [];
430  }
431  $this->relations['bytype'][$otherType][] = $relation;
432  $this->relations['bytype'][$this->persistenceFacade->getSimpleType($otherType)][] = $relation;
433  }
434  }
435  }
436 
437  /**
438  * Initialize attributes.
439  */
440  private function initAttributes() {
441  if ($this->attributes == null) {
442  $this->attributes = [];
443  $this->attributes['byname'] = [];
444  $this->attributes['refs'] = [];
445 
446  $attributes = $this->getAttributes();
447  foreach ($attributes as $attribute) {
448  $this->attributes['byname'][$attribute->getName()] = $attribute;
449  if ($attribute instanceof ReferenceDescription) {
450  $this->attributes['refs'][] = $attribute;
451  }
452  }
453  }
454  }
455 
456  /**
457  * @see PersistenceFacade::load()
458  * @note Precondition: Object rights have been checked already
459  */
460  abstract protected function loadImpl(ObjectId $oid, $buildDepth=BuildDepth::SINGLE);
461 
462  /**
463  * @see PersistenceFacade::create()
464  * @note Precondition: Object rights have been checked already
465  */
466  abstract protected function createImpl($type, $buildDepth=BuildDepth::SINGLE);
467 
468  /**
469  * @see PersistenceMapper::save()
470  * @note Precondition: Object rights have been checked already
471  */
472  abstract protected function saveImpl(PersistentObject $object);
473 
474  /**
475  * @see PersistenceMapper::delete()
476  * @note Precondition: Object rights have been checked already
477  */
478  abstract protected function deleteImpl(PersistentObject $object);
479 
480  /**
481  * @see PersistenceMapper::getOIDs()
482  */
483  abstract protected function getOIDsImpl($type, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null);
484 
485  /**
486  * @see PersistenceMapper::loadObjects()
487  */
488  abstract protected function loadObjectsImpl($type, $buildDepth=BuildDepth::SINGLE, $criteria=null, $orderby=null,
489  PagingInfo $pagingInfo=null);
490 
491  /**
492  * @see PersistenceMapper::loadRelation()
493  */
494  abstract protected function loadRelationImpl(array $objects, $role, $buildDepth=BuildDepth::SINGLE, $criteria=null,
495  $orderby=null, PagingInfo $pagingInfo=null);
496 }
497 ?>
OutputStrategy defines the interface for classes that write an object's content to a destination (cal...
containsDummyIds()
Check if this object id contains a dummy id.
Definition: ObjectId.php:247
loadObjects($type, $buildDepth=BuildDepth::SINGLE, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null)
getRelations($hierarchyType='all')
Get the relations for this type.
EventManager is responsible for dispatching events to registered listeners.
load(ObjectId $oid, $buildDepth=BuildDepth::SINGLE)
getOIDs($type, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null)
deleteImpl(PersistentObject $object)
getType()
Get the entity type that this mapper handles.
PersistenceException signals an exception in the persistence service.
logAction(PersistentObject $obj)
Log the state of the given object.
getOID()
Get the object id of the PersistentObject.
setLogStrategy(OutputStrategy $logStrategy)
Set the OutputStrategy used for logging persistence actions.
getChangedValues()
Get the list of changed attributes since creation, loading.
PersistenceMapper defines the interface for all mapper classes.
create($type, $buildDepth=BuildDepth::SINGLE)
getText($message, $parameters=null, $lang='')
Get a localized string.
validateValues()
Validate all values by calling PersistentObject::validateValue() Throws a ValidationException in case...
authorizationFailedError($resource, $action)
Handle an authorization error.
saveImpl(PersistentObject $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
beforeInsert()
This method is called once before inserting the newly created object into the store.
Instances of ReferenceDescription describe reference attributes of PersistentObjects.
getAttributeDisplayName($name, Message $message)
PersistentEvent signals create/update/delete operations on a persistent entity.
PersistenceFacade defines the interface for PersistenceFacade implementations.
static getStackTrace()
Get the stack trace.
afterInsert()
This method is called once after inserting the newly created object into the store.
ConcurrencyManager is used to handle concurrency for objects.
beforeUpdate()
This method is called always before updating the modified object in the store.
getOIDsImpl($type, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null)
loadImpl(ObjectId $oid, $buildDepth=BuildDepth::SINGLE)
loadRelation(array $objects, $role, $buildDepth=BuildDepth::SINGLE, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null)
static getLogger($name)
Get the logger with the given name.
Definition: LogManager.php:37
static getInstance($name, $dynamicConfiguration=[])
AbstractMapper provides a basic implementation for other mapper classes.
getAttributes(array $tags=[], $matchMode='all')
PersistentObject values may be tagged with application specific tags.
PagingInfo contains information about a paged list.
Definition: PagingInfo.php:18
PersistentObject defines the interface of all persistent objects.
checkAuthorization($resource, $action)
Check authorization on a resource (type/instance/instance property) and a given action.
static isValid($oid)
Check if a serialized ObjectId has a valid syntax, the type is known and if the number of primary key...
Definition: ObjectId.php:123
__construct(PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ConcurrencyManager $concurrencyManager, EventManager $eventManager)
Constructor.
getState()
Get the object's state:
PermissionManager implementations are used to handle all authorization requests.
LogManager is used to retrieve Logger instances.
Definition: LogManager.php:20
loadObjectsImpl($type, $buildDepth=BuildDepth::SINGLE, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null)
PersistenceAction values are used to define actions on PersistentObject instances.
createImpl($type, $buildDepth=BuildDepth::SINGLE)
loadRelationImpl(array $objects, $role, $buildDepth=BuildDepth::SINGLE, $criteria=null, $orderby=null, PagingInfo $pagingInfo=null)
ObjectFactory implements the service locator pattern by wrapping a Factory instance and providing sta...
ErrorHandler catches all php errors and transforms fatal errors into ErrorExceptions and non-fatal in...
getAttributeDescription($name, Message $message)
afterUpdate()
This method is called always after updating the modified object in the store.
AuthorizationException signals an exception in authorization.
Message is used to get localized messages to be used in the user interface.
Definition: Message.php:23