Completed
Push — master ( 8ee2ee...28e3f6 )
by Peter
06:52
created

ParentChildTrashHandlers   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 150
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 7

Test Coverage

Coverage 85.33%

Importance

Changes 0
Metric Value
wmc 15
lcom 0
cbo 7
dl 0
loc 150
ccs 64
cts 75
cp 0.8533
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
B registerChild() 0 27 4
A getPk() 0 6 1
D registerParent() 0 97 10
1
<?php
2
3
/**
4
 * This software package is licensed under AGPL or Commercial license.
5
 *
6
 * @package maslosoft/mangan
7
 * @licence AGPL or Commercial
8
 * @copyright Copyright (c) Piotr Masełkowski <[email protected]>
9
 * @copyright Copyright (c) Maslosoft
10
 * @copyright Copyright (c) Others as mentioned in code
11
 * @link https://maslosoft.com/mangan/
12
 */
13
14
namespace Maslosoft\Mangan\Helpers;
15
16
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
17
use Maslosoft\Addendum\Utilities\ClassChecker;
18
use Maslosoft\Mangan\Criteria;
19
use Maslosoft\Mangan\Events\Event;
20
use Maslosoft\Mangan\Events\ModelEvent;
21
use Maslosoft\Mangan\Events\RestoreEvent;
22
use Maslosoft\Mangan\Helpers\Sanitizer\Sanitizer;
23
use Maslosoft\Mangan\Interfaces\EntityManagerInterface;
24
use Maslosoft\Mangan\Interfaces\TrashInterface;
25
use UnexpectedValueException;
26
27
/**
28
 * OwneredTrashHandlers
29
 * Use this class to create trash handlers for ownered items.
30
 *
31
 * This class provides event handlers to properly manage trash, however it is
32
 * optional, so ownered and trashable can be handled by some custom methods.
33
 * These handles are not automatically registered.
34
 *
35
 * NOTE: Register **only once per type**, or it will not work properly.
36
 *
37
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
38
 */
39
class ParentChildTrashHandlers
40
{
41
42
	/**
43
	 * Register event handlers for parent of parent-child relation.
44
	 *
45
	 * @param AnnotatedInterface $parent
46
	 * @param string $childClass
47
	 */
48 4
	public function registerParent(AnnotatedInterface $parent, $childClass)
49
	{
50 4
		if (!ClassChecker::exists($childClass))
51
		{
52
			throw new UnexpectedValueException(sprintf('Class `%s` not found', $childClass));
53
		}
54
		// Delete all of this child items after removing from trash
55 4
		$beforeDelete = function(ModelEvent $event) use($parent, $childClass)
56
		{
57 1
			$model = $event->sender;
58 1
			$event->isValid = true;
59 1
			if ($model instanceof $parent)
60
			{
61
				$child = new $childClass;
62
				$criteria = new Criteria(null, $child);
63
				$criteria->parentId = $this->getPk($model);
64
65
				$event->isValid = $child->deleteAll($criteria);
66
			}
67 4
		};
68 4
		$beforeDelete->bindTo($this);
69 4
		Event::on($parent, EntityManagerInterface::EventBeforeDelete, $beforeDelete);
70
71
		// Trash all child items from parent item
72 4
		$afterTrash = function(ModelEvent $event)use($parent, $childClass)
73
		{
74 4
			$model = $event->sender;
75 4
			$event->isValid = true;
76 4
			if ($model instanceof $parent)
77
			{
78 4
				$child = new $childClass;
79 4
				$criteria = new Criteria(null, $child);
80 4
				$criteria->parentId = $this->getPk($model);
81
82 4
				$items = $child->findAll($criteria);
83
84
				// No items found, so skip
85 4
				if (empty($items))
86
				{
87
					$event->isValid = true;
88
					return true;
89
				}
90
91
				// Trash in loop all items
92 4
				foreach ($items as $item)
93
				{
94 4
					if (!$item->trash())
95
					{
96
						$event->isValid = false;
97 4
						return false;
98
					}
99
				}
100
			}
101 4
		};
102 4
		$afterTrash->bindTo($this);
103 4
		Event::on($parent, TrashInterface::EventAfterTrash, $afterTrash);
104
105
		// Restore all child items from parent, but only those after it was trashed.
106
		// This will keep previously trashed items in trash
107 4
		$afterRestore = function(RestoreEvent $event)use($parent, $childClass)
108
		{
109 2
			$model = $event->sender;
110 2
			if ($model instanceof $parent)
111
			{
112 2
				$child = new $childClass;
113 2
				$trash = $event->getTrash();
114 2
				$criteria = new Criteria(null, $trash);
115
116
				// Conditions decorator do not work with dots so sanitize manually.
117 2
				$s = new Sanitizer($child);
118
119 2
				$id = $s->write('parentId', $this->getPk($model));
120 2
				$criteria->addCond('data.parentId', '==', $id);
121
122
				// Restore only child items trashed when parent was trashed.
123
				// Skip earlier items
124 2
				assert(isset($trash->createDate), sprintf('When implementing `%s`, `createDate` field is required and must be set to date of deletion', TrashInterface::class));
125 2
				$criteria->addCond('createDate', 'gte', $trash->createDate);
126
127 2
				$trashedItems = $trash->findAll($criteria);
128 2
				if (empty($trashedItems))
129
				{
130
					$event->isValid = true;
131
					return true;
132
				}
133
134
				// Restore all items
135 2
				foreach ($trashedItems as $trashedItem)
136
				{
137 2
					$trashedItem->restore();
138
				}
139
			}
140 2
			$event->isValid = true;
141 4
		};
142 4
		$afterRestore->bindTo($this);
143 4
		Event::on($parent, TrashInterface::EventAfterRestore, $afterRestore);
144 4
	}
145
146
	/**
147
	 * Register event handlers for child item of parent-child relation.
148
	 *
149
	 * @param AnnotatedInterface $child
150
	 * @param string $parentClass
151
	 * @throws UnexpectedValueException
152
	 */
153 4
	public function registerChild(AnnotatedInterface $child, $parentClass)
154
	{
155 4
		if (!ClassChecker::exists($parentClass))
156
		{
157
			throw new UnexpectedValueException(sprintf('Class `%s` not found', $parentClass));
158
		}
159
		// Prevent restoring item if parent does not exists
160 4
		$beforeRestore = function(ModelEvent $event)use($child, $parentClass)
161
		{
162 3
			$model = $event->sender;
163
164 3
			if ($model instanceof $child)
165
			{
166 3
				$parent = new $parentClass;
167 3
				$criteria = new Criteria(null, $parent);
168 3
				assert(isset($model->parentId));
169 3
				$criteria->_id = $model->parentId;
170 3
				if (!$parent->exists($criteria))
171
				{
172 1
					$event->isValid = false;
173 1
					return false;
174
				}
175
			}
176 2
			$event->isValid = true;
177 4
		};
178 4
		Event::on($child, TrashInterface::EventBeforeRestore, $beforeRestore);
179 4
	}
180
181 4
	private function getPk(AnnotatedInterface $model)
182
	{
183 4
		$pk = PkManager::getFromModel($model);
184 4
		assert(!is_array($pk), 'Composite PK of `%s` not allowed for parentId');
185 4
		return $pk;
186
	}
187
188
}
189