1 | <?php |
||||||
2 | |||||||
3 | namespace DNADesign\Elemental\TopPage; |
||||||
4 | |||||||
5 | use DNADesign\Elemental\Extensions\ElementalPageExtension; |
||||||
6 | use DNADesign\Elemental\Models\ElementalArea; |
||||||
7 | use Page; |
||||||
8 | use SilverStripe\CMS\Model\SiteTreeExtension as BaseSiteTreeExtension; |
||||||
9 | use SilverStripe\ORM\DataObject; |
||||||
10 | use SilverStripe\ORM\ValidationException; |
||||||
11 | |||||||
12 | /** |
||||||
13 | * Class SiteTreeExtension |
||||||
14 | * |
||||||
15 | * This extension must be present on pagetypes that need to support Elemental TopPage functionality. |
||||||
16 | * It can be applied directly to Page, as it only takes effect in the presence of a ElementalArea. |
||||||
17 | * |
||||||
18 | * @property Page|$this $owner |
||||||
19 | * @package DNADesign\Elemental\TopPage |
||||||
20 | */ |
||||||
21 | class SiteTreeExtension extends BaseSiteTreeExtension |
||||||
22 | { |
||||||
23 | /** |
||||||
24 | * List of pages currently undergoing duplication |
||||||
25 | * |
||||||
26 | * @var array |
||||||
27 | */ |
||||||
28 | protected $duplicatedPages = []; |
||||||
29 | |||||||
30 | /** |
||||||
31 | * List of objects that need to udate their top page reference |
||||||
32 | * |
||||||
33 | * @var array |
||||||
34 | */ |
||||||
35 | protected $duplicatedObjects = []; |
||||||
36 | |||||||
37 | /** |
||||||
38 | * Extension point in @see DataObject::onAfterWrite() |
||||||
39 | * |
||||||
40 | * @throws ValidationException |
||||||
41 | */ |
||||||
42 | public function onAfterWrite(): void |
||||||
43 | { |
||||||
44 | $this->setTopPageForElementalArea(); |
||||||
45 | $this->processDuplicationFromOriginal(); |
||||||
46 | } |
||||||
47 | |||||||
48 | /** |
||||||
49 | * Extension point in @see DataObject::duplicate() |
||||||
50 | * |
||||||
51 | * @param Page $original |
||||||
52 | */ |
||||||
53 | public function onBeforeDuplicate(Page $original): void |
||||||
54 | { |
||||||
55 | $this->initDuplication($original); |
||||||
56 | } |
||||||
57 | |||||||
58 | /** |
||||||
59 | * Extension point in @see DataObject::duplicate() |
||||||
60 | * |
||||||
61 | * @param Page $original |
||||||
62 | * @param bool $doWrite |
||||||
63 | * @throws ValidationException |
||||||
64 | */ |
||||||
65 | public function onAfterDuplicate(Page $original, $doWrite): void |
||||||
66 | { |
||||||
67 | $this->processDuplication($original, (bool) $doWrite); |
||||||
68 | } |
||||||
69 | |||||||
70 | /** |
||||||
71 | * Generates a unique key for the page |
||||||
72 | * |
||||||
73 | * @return string|null |
||||||
74 | */ |
||||||
75 | public function getDuplicationKey(): ?string |
||||||
76 | { |
||||||
77 | $owner = $this->owner; |
||||||
78 | |||||||
79 | if (!$owner->isInDB()) { |
||||||
0 ignored issues
–
show
|
|||||||
80 | return null; |
||||||
81 | } |
||||||
82 | |||||||
83 | return sprintf('%s-%d', $owner->ClassName, $owner->ID); |
||||||
0 ignored issues
–
show
|
|||||||
84 | } |
||||||
85 | |||||||
86 | /** |
||||||
87 | * Registers the given object to receive an updated TopPage reference after the duplication |
||||||
88 | * operation completes, ensuring the new Page is written to the database beforehand. |
||||||
89 | * |
||||||
90 | * The registry uses a stack-like structure to allow accurate tracking of objects during |
||||||
91 | * duplication operations that include nested pages. |
||||||
92 | * |
||||||
93 | * @param DataObject $object |
||||||
94 | */ |
||||||
95 | public function addDuplicatedObject(DataObject $object): void |
||||||
96 | { |
||||||
97 | if (!$object->hasExtension(DataExtension::class)) { |
||||||
98 | return; |
||||||
99 | } |
||||||
100 | |||||||
101 | $key = $this->getDuplicatedPageKey(); |
||||||
102 | |||||||
103 | if ($key === null) { |
||||||
104 | return; |
||||||
105 | } |
||||||
106 | |||||||
107 | if (array_key_exists($key, $this->duplicatedObjects ?? [])) { |
||||||
108 | array_unshift($this->duplicatedObjects[$key], $object); |
||||||
109 | |||||||
110 | return; |
||||||
111 | } |
||||||
112 | |||||||
113 | $this->duplicatedObjects[$key] = [$object]; |
||||||
114 | } |
||||||
115 | |||||||
116 | /** |
||||||
117 | * Find currently duplicated page |
||||||
118 | * note: this doesn't change any stored data |
||||||
119 | * |
||||||
120 | * @return string|null |
||||||
121 | */ |
||||||
122 | protected function getDuplicatedPageKey(): ?string |
||||||
123 | { |
||||||
124 | $pages = $this->duplicatedPages; |
||||||
125 | |||||||
126 | if (count($pages ?? []) === 0) { |
||||||
127 | return null; |
||||||
128 | } |
||||||
129 | |||||||
130 | return array_shift($pages); |
||||||
131 | } |
||||||
132 | |||||||
133 | /** |
||||||
134 | * @param Page|SiteTreeExtension $original |
||||||
135 | */ |
||||||
136 | protected function initDuplication(Page $original): void |
||||||
137 | { |
||||||
138 | /** @var DataExtension $extension */ |
||||||
139 | $extension = singleton(DataExtension::class); |
||||||
140 | |||||||
141 | if (!$extension->getTopPageUpdate()) { |
||||||
142 | return; |
||||||
143 | } |
||||||
144 | |||||||
145 | $key = $original->getDuplicationKey(); |
||||||
146 | |||||||
147 | if ($key === null) { |
||||||
148 | return; |
||||||
149 | } |
||||||
150 | |||||||
151 | if (in_array($key, $this->duplicatedPages ?? [])) { |
||||||
152 | // this should never happen as it would indicate a duplication loop |
||||||
153 | return; |
||||||
154 | } |
||||||
155 | |||||||
156 | array_unshift($this->duplicatedPages, $key); |
||||||
157 | } |
||||||
158 | |||||||
159 | /** |
||||||
160 | * Update top page reference during duplication process |
||||||
161 | * |
||||||
162 | * @param Page $original |
||||||
163 | * @param bool $written |
||||||
164 | * @throws ValidationException |
||||||
165 | */ |
||||||
166 | protected function processDuplication(Page $original, bool $written): void |
||||||
167 | { |
||||||
168 | /** @var DataExtension $extension */ |
||||||
169 | $extension = singleton(DataExtension::class); |
||||||
170 | |||||||
171 | if (!$extension->getTopPageUpdate()) { |
||||||
172 | return; |
||||||
173 | } |
||||||
174 | |||||||
175 | if ($written) { |
||||||
176 | $this->writeDuplication($original); |
||||||
177 | |||||||
178 | return; |
||||||
179 | } |
||||||
180 | |||||||
181 | // write may not be triggered as the page maybe have come up via relation |
||||||
182 | // in this case we have to delay the processing until the page is written |
||||||
183 | // store the origin reference on the object (in memory only) so we can pick it up later |
||||||
184 | $this->owner->duplicationOriginal = $original; |
||||||
0 ignored issues
–
show
|
|||||||
185 | } |
||||||
186 | |||||||
187 | /** |
||||||
188 | * Relevant only for duplicated object that were not written at the time of duplication |
||||||
189 | * |
||||||
190 | * @throws ValidationException |
||||||
191 | */ |
||||||
192 | protected function processDuplicationFromOriginal(): void |
||||||
193 | { |
||||||
194 | /** @var DataExtension $extension */ |
||||||
195 | $extension = singleton(DataExtension::class); |
||||||
196 | |||||||
197 | if (!$extension->getTopPageUpdate()) { |
||||||
198 | return; |
||||||
199 | } |
||||||
200 | |||||||
201 | $owner = $this->owner; |
||||||
202 | |||||||
203 | if (!isset($owner->duplicationOriginal)) { |
||||||
204 | return; |
||||||
205 | } |
||||||
206 | |||||||
207 | $original = $owner->duplicationOriginal; |
||||||
208 | |||||||
209 | if (!$original instanceof Page) { |
||||||
210 | return; |
||||||
211 | } |
||||||
212 | |||||||
213 | unset($owner->duplicationOriginal); |
||||||
214 | $this->writeDuplication($original); |
||||||
215 | } |
||||||
216 | |||||||
217 | /** |
||||||
218 | * @param Page|SiteTreeExtension $original |
||||||
219 | * @throws ValidationException |
||||||
220 | */ |
||||||
221 | protected function writeDuplication(Page $original): void |
||||||
222 | { |
||||||
223 | $key = $original->getDuplicationKey(); |
||||||
224 | $currentKey = $this->getDuplicatedPageKey(); |
||||||
225 | |||||||
226 | if ($key !== $currentKey) { |
||||||
227 | // should never happen but it indicates that the nesting hierarchy was incorrect |
||||||
228 | return; |
||||||
229 | } |
||||||
230 | |||||||
231 | if (array_key_exists($key, $this->duplicatedObjects ?? [])) { |
||||||
232 | $objects = $this->duplicatedObjects[$key]; |
||||||
233 | |||||||
234 | /** @var DataObject|DataExtension $object */ |
||||||
235 | foreach ($objects as $object) { |
||||||
236 | // attach current page ID to the object |
||||||
237 | $object->setTopPage($this->owner); |
||||||
238 | } |
||||||
239 | } |
||||||
240 | |||||||
241 | // mark page as processed |
||||||
242 | array_shift($this->duplicatedPages); |
||||||
243 | } |
||||||
244 | |||||||
245 | /** |
||||||
246 | * Elemental area is created before related page is written so we have to set top page explicitly |
||||||
247 | * after page is written and the relations are available |
||||||
248 | * |
||||||
249 | * @throws ValidationException |
||||||
250 | */ |
||||||
251 | protected function setTopPageForElementalArea(): void |
||||||
252 | { |
||||||
253 | /** @var DataExtension $extension */ |
||||||
254 | $extension = singleton(DataExtension::class); |
||||||
255 | |||||||
256 | if (!$extension->getTopPageUpdate()) { |
||||||
257 | return; |
||||||
258 | } |
||||||
259 | |||||||
260 | /** @var Page|ElementalPageExtension $owner */ |
||||||
261 | $owner = $this->owner; |
||||||
262 | |||||||
263 | if (!$owner->hasExtension(ElementalPageExtension::class)) { |
||||||
264 | return; |
||||||
265 | } |
||||||
266 | |||||||
267 | if (!$owner->ElementalAreaID) { |
||||||
268 | return; |
||||||
269 | } |
||||||
270 | |||||||
271 | /** @var ElementalArea|DataExtension $area */ |
||||||
272 | $area = $owner->ElementalArea(); |
||||||
273 | |||||||
274 | if (!$area->exists()) { |
||||||
0 ignored issues
–
show
The method
exists() does not exist on DNADesign\Elemental\TopPage\DataExtension .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
275 | return; |
||||||
276 | } |
||||||
277 | |||||||
278 | if (!$area->hasExtension(DataExtension::class)) { |
||||||
0 ignored issues
–
show
The method
hasExtension() does not exist on DNADesign\Elemental\TopPage\DataExtension .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
279 | return; |
||||||
280 | } |
||||||
281 | |||||||
282 | $area->setTopPage($owner); |
||||||
0 ignored issues
–
show
The method
setTopPage() does not exist on DNADesign\Elemental\Models\ElementalArea . Since you implemented __call , consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
283 | } |
||||||
284 | } |
||||||
285 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.