-
Notifications
You must be signed in to change notification settings - Fork 116
/
DataExtension.php
216 lines (181 loc) · 5.49 KB
/
DataExtension.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
<?php
namespace DNADesign\Elemental\TopPage;
use DNADesign\Elemental\Models\BaseElement;
use DNADesign\Elemental\Models\ElementalArea;
use Page;
use SilverStripe\ORM\DataExtension as BaseDataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Versioned\Versioned;
/**
* Class DataExtension
*
* Top page data cache for improved performance
* intended owners of this extension are @see BaseElement and @see ElementalArea
* applying this extension to just one of these owners will not hinder top page functionality
* but the performance gain will be smaller
* it is recommended to apply this extension to BaseElement and for setups with deeper block nesting
* it is recommended to cover ElementalArea as well
*
* @property int $TopPageID
* @method Page TopPage()
* @property BaseElement|ElementalArea|$this $owner
* @package DNADesign\Elemental\TopPage
*/
class DataExtension extends BaseDataExtension
{
/**
* @config
* @var array
*/
private static $has_one = [
'TopPage' => Page::class,
];
/**
* @config
* @var array
*/
private static $indexes = [
'TopPageID' => true,
];
/**
* @var bool
*/
private $skipTopPageUpdate = false;
/**
* Exension point in @see DataObject::onAfterWrite()
*/
public function onAfterWrite(): void
{
$this->setTopPage();
}
/**
* Exension point in @see DataObject::duplicate()
*/
public function onBeforeDuplicate(): void
{
$this->clearTopPage();
}
/**
* Exension point in @see DataObject::duplicate()
*/
public function onAfterDuplicate(): void
{
$this->updateTopPage();
}
/**
* Find top level page of a block or elemental area
* this is very useful in case blocks are deeply nested
*
* for example:
* page -> elemental area -> block -> elemental area -> block
*
* this lookup is very performant as is safe to use in a template as well
*
* @return Page|null
* @throws ValidationException
*/
public function getTopPage(): ?Page
{
$list = [$this->owner];
while (count($list) > 0) {
/** @var DataObject|DataExtension $item */
$item = array_shift($list);
if ($item instanceof Page) {
// trivial case
return $item;
}
if ($item->hasExtension(DataExtension::class) && $item->TopPageID > 0) {
// top page is stored inside data object - just fetch it via cached call
$page = Page::get_by_id($item->TopPageID);
if ($page !== null && $page->exists()) {
return $page;
}
}
if ($item instanceof BaseElement) {
// parent lookup via block
$parent = $item->Parent();
if ($parent !== null && $parent->exists()) {
array_push($list, $parent);
}
continue;
}
if ($item instanceof ElementalArea) {
// parent lookup via elemental area
$parent = $item->getOwnerPage();
if ($parent !== null && $parent->exists()) {
array_push($list, $parent);
}
continue;
}
}
return null;
}
/**
* @param Page|null $page
* @throws ValidationException
*/
public function setTopPage(?Page $page = null): void
{
if ($this->skipTopPageUpdate) {
return;
}
/** @var BaseElement|ElementalArea|Versioned|DataExtension $owner */
$owner = $this->owner;
if (!$owner->hasExtension(DataExtension::class)) {
return;
}
if ($owner->TopPageID > 0) {
return;
}
$page = $page ?? $owner->getTopPage();
if ($page === null) {
return;
}
// set the page to properties in case this object is re-used later
$this->assignTopPage($page);
if ($owner->hasExtension(Versioned::class)) {
$owner->writeWithoutVersion();
return;
}
$owner->write();
}
/**
* Use this to wrap any code which is supposed to run without doing any top page updates
*
* @param callable $callback
* @return mixed
*/
public function withoutTopPageUpdate(callable $callback)
{
$this->skipTopPageUpdate = true;
try {
return $callback();
} finally {
$this->skipTopPageUpdate = false;
}
}
/**
* Register the object for top page update
* this is a little bit roundabout way to do it, but it's necessary because when cloned object is written
* the relations are not yet written so it's impossible to do a parent lookup at that time
*/
protected function updateTopPage(): void
{
/** @var SiteTreeExtension $extension */
$extension = singleton(SiteTreeExtension::class);
$extension->addDuplicatedObject($this->owner);
}
protected function assignTopPage(Page $page): void
{
$this->owner->TopPageID = (int) $page->ID;
}
/**
* Clears top page relation, this is useful when duplicating object as the new object doesn't necessarily
* belong to the original page
*/
protected function clearTopPage(): void
{
$this->owner->TopPageID = 0;
}
}