1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Thinktomorrow\Chief\PageBuilder; |
6
|
|
|
|
7
|
|
|
use Illuminate\Support\Collection; |
8
|
|
|
use Thinktomorrow\Chief\Sets\SetReference; |
9
|
|
|
use Thinktomorrow\Chief\Modules\TextModule; |
10
|
|
|
use Thinktomorrow\Chief\Relations\Relation; |
11
|
|
|
use Thinktomorrow\Chief\Relations\ActsAsParent; |
12
|
|
|
use Thinktomorrow\Chief\Modules\PagetitleModule; |
13
|
|
|
use Thinktomorrow\Chief\Modules\Application\CreateModule; |
14
|
|
|
use Thinktomorrow\Chief\Modules\Application\UpdateModule; |
15
|
|
|
use Thinktomorrow\Chief\FlatReferences\FlatReferenceFactory; |
16
|
|
|
use Thinktomorrow\Chief\FlatReferences\FlatReferenceCollection; |
17
|
|
|
use Thinktomorrow\Chief\Sets\StoredSetReference; |
18
|
|
|
|
19
|
|
|
class UpdateSections |
20
|
|
|
{ |
21
|
|
|
/** @var ActsAsParent */ |
22
|
|
|
private $model; |
23
|
|
|
|
24
|
|
|
/** @var array */ |
25
|
|
|
private $relation_references; |
26
|
|
|
|
27
|
|
|
/** @var array */ |
28
|
|
|
private $text_modules; |
29
|
|
|
|
30
|
|
|
/**@var array */ |
31
|
42 |
|
private $set_refs; |
32
|
|
|
|
33
|
42 |
|
/** @var array */ |
34
|
42 |
|
private $sorting; |
35
|
42 |
|
|
36
|
42 |
|
final private function __construct(ActsAsParent $model, array $relation_references, array $text_modules, array $set_refs, array $sorting) |
37
|
42 |
|
{ |
38
|
42 |
|
$this->model = $model; |
39
|
|
|
$this->relation_references = $relation_references; |
40
|
42 |
|
$this->text_modules = $text_modules; |
41
|
|
|
$this->set_refs = $set_refs; |
42
|
42 |
|
$this->sorting = $sorting; |
43
|
|
|
} |
44
|
|
|
|
45
|
42 |
|
public static function forModel(ActsAsParent $model, array $relation_references, array $text_modules, array $set_refs, array $sorting) |
46
|
|
|
{ |
47
|
|
|
return new static($model, $relation_references, $text_modules, $set_refs, $sorting); |
48
|
42 |
|
} |
49
|
|
|
|
50
|
42 |
|
public function updateModules() |
51
|
38 |
|
{ |
52
|
|
|
$referred_instances = FlatReferenceCollection::fromFlatReferences($this->relation_references); |
53
|
|
|
|
54
|
4 |
|
$this->removeModules($referred_instances); |
55
|
|
|
|
56
|
4 |
|
foreach ($referred_instances as $i => $instance) { |
57
|
4 |
|
if(!$relation = Relation::find($this->model->getMorphClass(), $this->model->id, $instance->getMorphClass(), $instance->id)) { |
|
|
|
|
58
|
|
|
$this->model->adoptChild($instance, ['sort' => $i]); |
59
|
|
|
} |
60
|
4 |
|
} |
61
|
|
|
|
62
|
|
|
return $this; |
63
|
42 |
|
} |
64
|
|
|
|
65
|
42 |
|
public function updateSets() |
66
|
|
|
{ |
67
|
42 |
|
$stored_set_refs = collect($this->set_refs)->reject(function($ref){ |
68
|
2 |
|
return !$ref; |
69
|
|
|
})->map(function($flat_set_ref){ |
70
|
|
|
return $this->findOrCreateStoredSetReference($flat_set_ref); |
71
|
|
|
}); |
72
|
2 |
|
|
73
|
|
|
$this->removeSets($stored_set_refs); |
|
|
|
|
74
|
2 |
|
|
75
|
|
|
foreach ($stored_set_refs as $i => $stored_set_ref) { |
76
|
|
|
if(!$relation = Relation::find($this->model->getMorphClass(), $this->model->id, $stored_set_ref->getMorphClass(), $stored_set_ref->id)) { |
|
|
|
|
77
|
42 |
|
$this->model->adoptChild($stored_set_ref, ['sort' => $i]); |
78
|
|
|
} |
79
|
|
|
} |
80
|
42 |
|
|
81
|
|
|
return $this; |
82
|
42 |
|
} |
83
|
11 |
|
|
84
|
7 |
|
/** |
85
|
|
|
* Get all module children and only those not passed so we know to delete these ones |
86
|
4 |
|
* @param Collection $referred_instances |
87
|
|
|
*/ |
88
|
42 |
|
private function removeModules(Collection $referred_instances): void |
89
|
|
|
{ |
90
|
42 |
|
$this->model->children()->filter(function ($instance) { |
91
|
|
|
return (!$instance instanceof StoredSetReference && !$instance instanceof TextModule && !$instance instanceof PagetitleModule); |
92
|
42 |
|
})->reject(function ($instance) use ($referred_instances) { |
93
|
10 |
|
// If this model still exists in the update request, we'll reject it from the removed list. |
94
|
10 |
|
return $referred_instances->filter(function ($referred_instance) use ($instance) { |
95
|
|
|
return (get_class($referred_instance) == get_class($instance) && $referred_instance->id == $instance->id); |
96
|
|
|
})->isNotEmpty(); |
97
|
|
|
})->each(function ($instance) { |
98
|
42 |
|
$this->model->rejectChild($instance); |
99
|
|
|
}); |
100
|
42 |
|
} |
101
|
|
|
|
102
|
42 |
|
/** |
103
|
37 |
|
* Get all module children and only those not passed so we know to delete these ones |
104
|
|
|
* @param Collection $referred_instances |
105
|
|
|
*/ |
106
|
5 |
|
private function removeSets(Collection $referred_instances): void |
107
|
|
|
{ |
108
|
|
|
$this->model->children()->filter(function ($instance) { |
109
|
5 |
|
return ($instance instanceof StoredSetReference); |
110
|
1 |
|
})->reject(function ($instance) use ($referred_instances) { |
111
|
1 |
|
// If this model still exists in the update request, we'll reject it from the removed list. |
112
|
1 |
|
return $referred_instances->filter(function ($referred_instance) use ($instance) { |
113
|
1 |
|
return (get_class($referred_instance) == get_class($instance) && $referred_instance->id == $instance->id); |
114
|
|
|
})->isNotEmpty(); |
115
|
|
|
})->each(function ($instance) { |
116
|
|
|
$this->model->rejectChild($instance); |
117
|
|
|
}); |
118
|
|
|
} |
119
|
4 |
|
|
120
|
4 |
|
// private function removeExistingModules() |
121
|
4 |
|
// { |
122
|
4 |
|
// foreach ($this->model->children() as $instance) { |
123
|
|
|
// if ($instance instanceof StoredSetReference || $instance instanceof TextModule || $instance instanceof PagetitleModule) { |
124
|
|
|
// continue; |
125
|
|
|
// } |
126
|
|
|
// $this->model->rejectChild($instance); |
127
|
5 |
|
// } |
128
|
|
|
// } |
129
|
|
|
// |
130
|
5 |
|
// private function removeExistingSets() |
131
|
|
|
// { |
132
|
|
|
// foreach ($this->model->children() as $instance) { |
133
|
5 |
|
// if (!$instance instanceof StoredSetReference) { |
134
|
5 |
|
// continue; |
135
|
|
|
// } |
136
|
|
|
// $this->model->rejectChild($instance); |
137
|
5 |
|
// } |
138
|
|
|
// } |
139
|
|
|
|
140
|
42 |
|
public function addTextModules() |
141
|
|
|
{ |
142
|
42 |
|
if (!isset($this->text_modules['new']) || empty($this->text_modules['new'])) { |
143
|
35 |
|
return $this; |
144
|
|
|
} |
145
|
|
|
|
146
|
7 |
|
foreach ($this->text_modules['new'] as $text_module) { |
147
|
7 |
|
|
148
|
|
|
// Create pagetitle text module |
149
|
|
|
if (isset($text_module['type']) && $text_module['type'] == 'pagetitle') { |
150
|
|
|
$module = app(CreateModule::class)->handle( |
151
|
|
|
(new PagetitleModule())->morphKey(), |
152
|
7 |
|
$text_module['slug'], |
153
|
3 |
|
$this->model->getMorphClass(), |
154
|
3 |
|
$this->model->id |
|
|
|
|
155
|
|
|
); |
156
|
|
|
} // Create page specific text module |
157
|
4 |
|
else { |
158
|
4 |
|
$module = app(CreateModule::class)->handle( |
159
|
|
|
(new TextModule())->morphKey(), |
160
|
|
|
$text_module['slug'], |
161
|
|
|
$this->model->getMorphClass(), |
162
|
4 |
|
$this->model->id |
163
|
|
|
); |
164
|
|
|
} |
165
|
7 |
|
|
166
|
|
|
// Connect to page - sorting will be set later on... |
167
|
|
|
$this->model->adoptChild($module, ['sort' => 0]); |
168
|
3 |
|
|
169
|
|
|
// Add content |
170
|
3 |
|
app(UpdateModule::class)->handle($module->id, $module->slug, $text_module['trans'], [], []); |
171
|
|
|
|
172
|
|
|
// Change slug representation in sorting to proper flat reference |
173
|
|
|
$index = (false !== $key = array_search($module->slug, $this->sorting)) ? $key : null; |
174
|
3 |
|
$this->sorting[$index] = $module->flatReference()->get(); |
175
|
|
|
} |
176
|
|
|
|
177
|
3 |
|
return $this; |
178
|
3 |
|
} |
179
|
|
|
|
180
|
42 |
|
public function updateTextModules() |
181
|
|
|
{ |
182
|
42 |
|
if (!isset($this->text_modules['replace']) || empty($this->text_modules['replace'])) { |
183
|
|
|
return $this; |
184
|
42 |
|
} |
185
|
|
|
|
186
|
|
|
foreach ($this->text_modules['replace'] as $text_module) { |
187
|
|
|
if (!$module = FlatReferenceFactory::fromString($text_module['id'])->instance()) { |
188
|
5 |
|
continue; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
// Do not update if content of text is completely empty. We will remove this module instead |
192
|
|
|
if ($this->isTextCompletelyEmpty($text_module['trans'])) { |
193
|
5 |
|
$this->removeTextualModule($module); |
194
|
5 |
|
continue; |
195
|
|
|
} |
196
|
5 |
|
|
197
|
|
|
foreach ($text_module['trans'] as $locale => $content) { |
198
|
|
|
$text_module['trans'][$locale]['content'] = $content['content']; |
199
|
|
|
} |
200
|
5 |
|
|
201
|
|
|
// Replace content |
202
|
|
|
app(UpdateModule::class)->handle($module->id, $module->slug, $text_module['trans'], [], []); |
203
|
42 |
|
} |
204
|
|
|
|
205
|
|
|
return $this; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
private function removeTextualModule($module) |
209
|
|
|
{ |
210
|
|
|
if (!$module instanceof TextModule && !$module instanceof PagetitleModule) { |
211
|
|
|
throw new \Exception('Invalid request to remove non textual module'); |
212
|
|
|
} |
213
|
7 |
|
|
214
|
|
|
$this->model->rejectChild($module); |
215
|
7 |
|
|
216
|
|
|
// In case of a textual module, we also delete the module itself |
217
|
7 |
|
$module->delete(); |
218
|
7 |
|
} |
219
|
7 |
|
|
220
|
7 |
|
public function sort() |
221
|
|
|
{ |
222
|
7 |
|
$children = $this->model->children(); |
223
|
4 |
|
|
224
|
7 |
|
foreach ($this->sorting as $sorting => $reference) { |
225
|
|
|
|
226
|
|
|
// Reference can be null in case that the module has been removed (empty selection). This will avoid |
227
|
|
|
// in case of duplicate module references that the removed module will be used for the sorting instead. |
228
|
|
|
if (!$reference) { |
229
|
7 |
|
continue; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
$child = $children->first(function ($c) use ($reference) { |
233
|
|
|
return $c->flatReference()->get() == $reference; |
234
|
|
|
}); |
235
|
|
|
|
236
|
|
|
if (!$child) { |
237
|
|
|
continue; |
238
|
|
|
} |
239
|
|
|
|
240
|
7 |
|
$this->model->sortChild($child, $sorting); |
|
|
|
|
241
|
|
|
} |
242
|
7 |
|
|
243
|
7 |
|
return $this; |
244
|
|
|
} |
245
|
|
|
|
246
|
7 |
|
/** |
247
|
|
|
* Do we consider the translation payload to be 'empty'. This means |
248
|
|
|
* that each line of the translation only contains spaces or empty tags. |
249
|
2 |
|
* |
250
|
|
|
* @param $trans |
251
|
2 |
|
* @return bool |
252
|
|
|
*/ |
253
|
|
|
private function isTextCompletelyEmpty($trans): bool |
254
|
2 |
|
{ |
255
|
1 |
|
$is_completely_empty = true; |
256
|
|
|
|
257
|
|
|
foreach ($trans as $locale => $lines) { |
258
|
1 |
|
foreach ($lines as $key => $line) { |
259
|
|
|
$stripped_line = $this->stripTagsBlacklist($line, ['p', 'br']); |
260
|
|
|
$stripped_line = trim($stripped_line); |
261
|
|
|
|
262
|
|
|
if ($stripped_line) { |
263
|
|
|
$is_completely_empty = false; |
264
|
|
|
break; |
265
|
|
|
} |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return $is_completely_empty; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Pass a list of not allowed tags as they will be stripped out from the value. |
274
|
|
|
* e.g. ['p', 'br' ] |
275
|
|
|
* |
276
|
|
|
* @param $value |
277
|
|
|
* @param array $blacklist |
278
|
|
|
* @return mixed |
279
|
|
|
*/ |
280
|
|
|
private function stripTagsBlacklist($value, $blacklist = []) |
281
|
|
|
{ |
282
|
|
|
foreach ($blacklist as $tag) { |
283
|
|
|
$value = preg_replace('/<\/?' . $tag . '(.|\s)*?>/', '', $value); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
return $value; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
private function findOrCreateStoredSetReference(string $flat_set_ref) |
290
|
|
|
{ |
291
|
|
|
list($className, $id) = explode('@', $flat_set_ref); |
292
|
|
|
|
293
|
|
|
/** If set reference is not stored yet, we will do this now */ |
294
|
|
|
if ($className == SetReference::class) { |
295
|
|
|
return SetReference::find($id)->store(); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
return FlatReferenceFactory::fromString($flat_set_ref)->instance(); |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|