1 <?php
2
3 /*
4 * This file is part of the Icybee package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Icybee\Modules\Nodes;
13
14 use ICanBoogie\ActiveRecord;
15 use ICanBoogie\DateTime;
16
17 use Icybee\Modules\Sites\Site;
18 use Icybee\Modules\Users\User;
19
20 /**
21 * A node representation.
22 *
23 * @property DateTime $created_at The date and time at which the node was created.
24 * @property DateTime $updated_at The date and time at which the node was updated.
25 * @property Node $native
26 * @property User $user The user owning the node.
27 * @property Site $site The site associated with the node.
28 * @property-read Node $next
29 * @property-read Node $previous
30 * @property-read Node $translation
31 * @property-read array[string]Node $translations
32 * @property-read array[string]int $translations_keys
33 * @property array[string]mixed $css_class_names {@see Node::get_css_class_names}.
34 * @property string $css_class {@see Node::get_css_class}.
35 */
36 class Node extends ActiveRecord implements \Brickrouge\CSSClassNames
37 {
38 use \Brickrouge\CSSClassNamesProperty;
39
40 const NID = 'nid';
41 const UID = 'uid';
42 const SITEID = 'siteid';
43 const UUID = 'uuid';
44 const TITLE = 'title';
45 const SLUG = 'slug';
46 const CONSTRUCTOR = 'constructor';
47 const CREATED_AT = 'created_at';
48 const UPDATED_AT = 'updated_at';
49 const IS_ONLINE = 'is_online';
50 const LANGUAGE = 'language';
51 const NATIVEID = 'nativeid';
52
53 /**
54 * Node key.
55 *
56 * @var int
57 */
58 public $nid;
59
60 /**
61 * Identifier of the owner of the node.
62 *
63 * @var int
64 */
65 public $uid = 0;
66
67 /**
68 * Return the user owning the node.
69 *
70 * @return User
71 */
72 protected function get_user()
73 {
74 return $this->uid ? ActiveRecord\get_model('users')->find($this->uid) : null;
75 }
76
77 /**
78 * Updates the {@link $uid} property using a {@link User} instance.
79 *
80 * @param User $user
81 */
82 protected function set_user(User $user)
83 {
84 $this->uid = $user->uid;
85 }
86
87 /**
88 * Identifier of the site the node belongs to.
89 *
90 * The property is empty of the node is not bound to a website.
91 *
92 * @var int
93 */
94 public $siteid = 0;
95
96 /**
97 * Returns the {@link Site} instance associated with the node.
98 *
99 * @return Site
100 */
101 protected function get_site()
102 {
103 return $this->siteid ? ActiveRecord\get_model('sites')->find($this->siteid) : null;
104 }
105
106 /**
107 * Updates the {@link $siteid} property using a {@link Site} instance.
108 *
109 * @param Site $site
110 */
111 protected function set_site(Site $site)
112 {
113 $this->siteid = $site->siteid;
114 }
115
116 /**
117 * A v4 UUID.
118 *
119 * @var string
120 */
121 public $uuid;
122
123 /**
124 * Title of the node.
125 *
126 * @var string
127 */
128 public $title;
129
130 /**
131 * Slug of the node.
132 *
133 * @var string
134 */
135 public $slug;
136
137 /**
138 * Returns the slug of the node.
139 *
140 * This function is only invoked if the {@link slug} property was empty during construct.
141 * By default it returns a normalized version of the {@link title} property.
142 *
143 * @return string
144 */
145 protected function get_slug()
146 {
147 return slugize($this->title, $this->language);
148 }
149
150 /**
151 * Constructor of the node.
152 *
153 * @var string
154 */
155 public $constructor;
156
157 /**
158 * Returns the constructor of the page.
159 *
160 * This function is only called if the {@link constructor} property was empty during construct.
161 * By default it returns the identifier of the model managing the node.
162 *
163 * @return string
164 */
165 protected function get_constructor()
166 {
167 return $this->model_id;
168 }
169
170 use ActiveRecord\CreatedAtProperty;
171 use ActiveRecord\UpdatedAtProperty;
172
173 /**
174 * Whether the node is online or not.
175 *
176 * @var bool
177 */
178 public $is_online = false;
179
180 /**
181 * Language of the node.
182 *
183 * The property is empty of the node is not bound to a language.
184 *
185 * @var string
186 */
187 public $language = '';
188
189 /**
190 * Returns the language for the page.
191 *
192 * This function is only called if the {@link language} property was empty during construct. By
193 * default it returns the language of the {@link site} associated with the node.
194 *
195 * @return string
196 */
197 protected function get_language()
198 {
199 return $this->site ? $this->site->language : null;
200 }
201
202 /**
203 * Identifier of the node this node is translating.
204 *
205 * The property is empty if the node is not translating another node.
206 *
207 * @var int
208 */
209 public $nativeid = 0;
210
211 /**
212 * Creates a Node instance.
213 *
214 * The following properties are unset if they are empty, so that their getter may return
215 * a fallback value:
216 *
217 * - {@link constructor}: Defaults to the model identifier. {@link get_constructor}.
218 * - {@link language}: Defaults to the associated site's language. {@link get_language}.
219 * - {@link slug}: Defaults to a normalize title. {@link get_slug}.
220 */
221 public function __construct($model='nodes')
222 {
223 if (empty($this->constructor))
224 {
225 unset($this->constructor);
226 }
227
228 if (empty($this->language))
229 {
230 unset($this->language);
231 }
232
233 if (empty($this->slug))
234 {
235 unset($this->slug);
236 }
237
238 parent::__construct($model);
239 }
240
241 /**
242 * Fires {@link \Brickrouge\AlterCSSClassNamesEvent} after the {@link $css_class_names} property
243 * was get.
244 */
245 public function __get($property)
246 {
247 $value = parent::__get($property);
248
249 if ($property === 'css_class_names')
250 {
251 new \Brickrouge\AlterCSSClassNamesEvent($this, $value);
252 }
253
254 return $value;
255 }
256
257 /**
258 * Obtains a UUID from the model if the {@link $uuid} property is empty.
259 */
260 public function save()
261 {
262 if (!$this->uuid)
263 {
264 $this->uuid = $this->model->obtain_uuid();
265 }
266
267 return parent::save();
268 }
269
270 /**
271 * Sets {@link $created_at} to "now" if it is empty, and sets {@link $updated_at} to "now"
272 * before passing the method to the parent class.
273 *
274 * Adds `language` if it is not defined.
275 */
276 protected function alter_persistent_properties(array $properties, \ICanBoogie\ActiveRecord\Model $model)
277 {
278 if ($this->get_created_at()->is_empty)
279 {
280 $this->set_created_at('now');
281 }
282
283 $this->set_updated_at('now');
284
285 return parent::alter_persistent_properties($properties, $model) + [
286
287 'language' => ''
288
289 ];
290 }
291
292 /**
293 * Return the previous visible sibling for the node.
294 *
295 * @return Node|bool
296 */
297 protected function lazy_get_previous()
298 {
299 return $this->model->own->visible
300 ->where('nid != ? AND created_at <= ?', $this->nid, $this->created_at)
301 ->order('created_at DESC')
302 ->one;
303 }
304
305 /**
306 * Return the next visible sibling for the node.
307 *
308 * @return Node|bool
309 */
310 protected function lazy_get_next()
311 {
312 return $this->model->own->visible
313 ->where('nid != ? AND created_at > ?', $this->nid, $this->created_at)
314 ->order('created_at')
315 ->one;
316 }
317
318 static private $translations_keys;
319
320 protected function lazy_get_translations_keys()
321 {
322 global $core;
323
324 $native_language = $this->siteid ? $this->site->native->language : $core->language;
325
326 if (!self::$translations_keys)
327 {
328 $groups = $core->models['nodes']->select('nativeid, nid, language')->where('nativeid != 0')->order('language')->all(\PDO::FETCH_GROUP | \PDO::FETCH_NUM);
329 $keys = [];
330
331 foreach ($groups as $native_id => $group)
332 {
333 foreach ($group as $row)
334 {
335 list($nativeid, $tlanguage) = $row;
336
337 $keys[$native_id][$nativeid] = $tlanguage;
338 }
339 }
340
341 foreach ($keys as $native_id => $translations)
342 {
343 $all = [ $native_id => $native_language ] + $translations;
344
345 foreach ($translations as $nativeid => $tlanguage)
346 {
347 $keys[$nativeid] = $all;
348 unset($keys[$nativeid][$nativeid]);
349 }
350 }
351
352 self::$translations_keys = $keys;
353 }
354
355 $nid = $this->nid;
356
357 return isset(self::$translations_keys[$nid]) ? self::$translations_keys[$nid] : null;
358 }
359
360 /**
361 * Returns the translation in the specified language for the record, or the record itself if no
362 * translation can be found.
363 *
364 * @param string $language The language for the translation. If the language is empty, the
365 * current language (as defined by `$core->language`) is used.
366 *
367 * @return Node The translation for the record, or the record itself if
368 * no translation could be found.
369 */
370 public function translation($language=null)
371 {
372 global $core;
373
374 if (!$language)
375 {
376 $language = $core->language;
377 }
378
379 $translations = $this->translations_keys;
380
381 if ($translations)
382 {
383 $translations = array_flip($translations);
384
385 if (isset($translations[$language]))
386 {
387 return $this->model->find($translations[$language]);
388 }
389 }
390
391 return $this;
392 }
393
394 protected function lazy_get_translation()
395 {
396 return $this->translation();
397 }
398
399 protected function lazy_get_translations()
400 {
401 $translations = $this->translations_keys;
402
403 if (!$translations)
404 {
405 return;
406 }
407
408 return $this->model->find(array_keys($translations));
409 }
410
411 /**
412 *
413 * Return the native node for this translated node.
414 */
415 protected function lazy_get_native()
416 {
417 return $this->nativeid ? $this->model[$this->nativeid] : $this;
418 }
419
420 /**
421 * Returns the CSS class names of the node.
422 *
423 * @return array[string]mixed
424 */
425 protected function get_css_class_names()
426 {
427 $nid = $this->nid;
428 $slug = $this->slug;
429
430 return [
431
432 'type' => 'node',
433 'id' => $nid ? "node-{$nid}" : null,
434 'slug' => $slug ? "node-slug-{$slug}" : null,
435 'constructor' => 'constructor-' . \ICanBoogie\normalize($this->constructor)
436
437 ];
438 }
439 }