Completed
Push — master ( b4e9b7...cff413 )
by Peter
11:12
created

ParentChildTrashHandlers   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 184
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 8

Test Coverage

Coverage 84.78%

Importance

Changes 0
Metric Value
wmc 20
lcom 0
cbo 8
dl 0
loc 184
c 0
b 0
f 0
ccs 78
cts 92
cp 0.8478
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A getPk() 0 6 1
D registerParent() 0 132 16
A registerChild() 0 26 3
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\OwneredInterface;
25
use Maslosoft\Mangan\Interfaces\TrashInterface;
26
use UnexpectedValueException;
27
28
/**
29
 * ParentChildTrashHandlers
30
 * Use this class to create trash handlers for owned items.
31
 *
32
 * This class provides event handlers to properly manage trash, however it is
33
 * optional, so owned and trashable can be handled by some custom methods.
34
 * These handles are not automatically registered.
35
 *
36
 * NOTE: Register **only once per type**, or it will not work properly.
37
 *
38
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
39
 */
40
class ParentChildTrashHandlers
41
{
42
43
	/**
44
	 * Register event handlers for parent of parent-child relation.
45
	 *
46
	 * @param AnnotatedInterface|string $parent
47
	 * @param string $childClass
48
	 */
49 1
	public function registerParent($parent, $childClass)
50
	{
51 1
		if (!ClassChecker::exists($childClass))
52
		{
53
			throw new UnexpectedValueException(sprintf('Class `%s` not found', $childClass));
54
		}
55
		// Delete all of this child items after removing from trash
56 1
		$beforeDelete = function(ModelEvent $event) use($parent, $childClass)
57
		{
58 1
			$model = $event->sender;
59 1
			$event->isValid = true;
60
61 1
			if (is_a($model, $parent))
62
			{
63
				$child = new $childClass;
64
				// Ensure owner
65
				if($child instanceof OwneredInterface)
66
				{
67
					$child->setOwner($model);
68
				}
69
				$criteria = new Criteria(null, $child);
70
				$criteria->parentId = $this->getPk($model);
71
72
				$event->isValid = $child->deleteAll($criteria);
73
			}
74 1
			return $event->isValid;
75 1
		};
76 1
		$beforeDelete->bindTo($this);
77 1
		Event::on($parent, EntityManagerInterface::EventBeforeDelete, $beforeDelete);
78
79
		// Trash all child items from parent item
80 1
		$afterTrash = function(ModelEvent $event)use($parent, $childClass)
81
		{
82 1
			$model = $event->sender;
83 1
			$event->isValid = true;
84 1
			if (is_a($model, $parent))
85
			{
86 1
				$child = new $childClass;
87
				// Ensure owner
88 1
				if($child instanceof OwneredInterface)
89
				{
90 1
					$child->setOwner($model);
91
				}
92 1
				$criteria = new Criteria(null, $child);
93 1
				$criteria->parentId = $this->getPk($model);
94
95 1
				$items = $child->findAll($criteria);
96
97
				// No items found, so skip
98 1
				if (empty($items))
99
				{
100
					$event->isValid = true;
101
					return $event->isValid;
102
				}
103
104
				// Trash in loop all items
105 1
				foreach ($items as $item)
106
				{
107
					// Ensure owner
108 1
					if($item instanceof OwneredInterface)
109
					{
110 1
						$item->setOwner($model);
111
					}
112 1
					if (!$item->trash())
113
					{
114
						$event->isValid = false;
115 1
						return $event->isValid;
116
					}
117
				}
118
			}
119 1
			return $event->isValid;
120 1
		};
121 1
		$afterTrash->bindTo($this);
122 1
		Event::on($parent, TrashInterface::EventAfterTrash, $afterTrash);
123
124
		// Restore all child items from parent, but only those after it was trashed.
125
		// This will keep previously trashed items in trash
126 1
		$afterRestore = function(RestoreEvent $event)use($parent, $childClass)
127
		{
128 1
			$model = $event->sender;
129 1
			if (is_a($model, $parent))
130
			{
131 1
				$child = new $childClass;
132
				// Ensure owner
133 1
				if($child instanceof OwneredInterface)
134
				{
135 1
					$child->setOwner($model);
136
				}
137 1
				$trash = $event->getTrash();
138 1
				$criteria = new Criteria(null, $trash);
139
140
				// Conditions decorator do not work with dots so sanitize manually.
141 1
				$s = new Sanitizer($child);
142
143 1
				$id = $s->write('parentId', $this->getPk($model));
144 1
				$criteria->addCond('data.parentId', '==', $id);
145
146
				// Restore only child items trashed when parent was trashed.
147
				// Skip earlier items
148 1
				assert(isset($trash->createDate), sprintf('When implementing `%s`, `createDate` field is required and must be set to date of deletion', TrashInterface::class));
149 1
				$criteria->addCond('createDate', 'gte', $trash->createDate);
150
151 1
				$trashedItems = $trash->findAll($criteria);
152 1
				if (empty($trashedItems))
153
				{
154
					$event->isValid = true;
155
					return $event->isValid;
156
				}
157
158
				// Restore all items
159 1
				$restored = [];
160 1
				foreach ($trashedItems as $trashedItem)
161
				{
162
					// Ensure owner
163 1
					if($trashedItem instanceof OwneredInterface)
164
					{
165 1
						$trashedItem->setOwner($model);
166
					}
167 1
					$restored[] = (int) $trashedItem->restore();
168
				}
169 1
				if(array_sum($restored) !== count($restored))
170
				{
171
					$event->isValid = false;
172
					return $event->isValid;
173
				}
174
			}
175 1
			$event->isValid = true;
176 1
			return $event->isValid;
177 1
		};
178 1
		$afterRestore->bindTo($this);
179 1
		Event::on($parent, TrashInterface::EventAfterRestore, $afterRestore);
180 1
	}
181
182
	/**
183
	 * Register event handlers for child item of parent-child relation.
184
	 *
185
	 * @param AnnotatedInterface|string $child
186
	 * @param string $parentClass
187
	 * @throws UnexpectedValueException
188
	 */
189 1
	public function registerChild($child, $parentClass)
190
	{
191 1
		assert(ClassChecker::exists($parentClass), new UnexpectedValueException(sprintf('Class `%s` not found', $parentClass)));
192
193
		// Prevent restoring item if parent does not exists
194 1
		$beforeRestore = function(ModelEvent $event)use($child, $parentClass)
195
		{
196 1
			$model = $event->sender;
197
198 1
			if (is_a($model, $child))
199
			{
200 1
				$parent = new $parentClass;
201 1
				$criteria = new Criteria(null, $parent);
202 1
				assert(isset($model->parentId));
203 1
				$criteria->_id = $model->parentId;
204 1
				if (!$parent->exists($criteria))
205
				{
206 1
					$event->isValid = false;
207 1
					return $event->isValid;
208
				}
209
			}
210 1
			$event->isValid = true;
211 1
			return $event->isValid;
212 1
		};
213 1
		Event::on($child, TrashInterface::EventBeforeRestore, $beforeRestore);
214 1
	}
215
216 1
	private function getPk(AnnotatedInterface $model)
217
	{
218 1
		$pk = PkManager::getFromModel($model);
219 1
		assert(!is_array($pk), 'Composite PK of `%s` not allowed for parentId');
220 1
		return $pk;
221
	}
222
223
}
224