1 <?php
2
3 /*
4 * This file is part of the ICanBoogie 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 ICanBoogie;
13
14 use ICanBoogie\ActiveRecord\Connection;
15 use ICanBoogie\ActiveRecord\Model;
16 use ICanBoogie\ActiveRecord\ModelNotDefined;
17 use ICanBoogie\I18n;
18
19 /**
20 * A module of the framework.
21 *
22 * @property-read array $description The description of the module.
23 * @property-read string $flat_id Underscored identifier.
24 * @property-read string $id The identifier of the module, defined by {@link T_ID}.
25 * @property-read Model $model The primary model of the module.
26 * @property-read Module $parent The parent module, defined by {@link T_EXTENDS}.
27 * @property-read string $path The path to the module, defined by {@link T_PATH}.
28 * @property-read string $title The localized title of the module.
29 */
30 class Module extends Object
31 {
32 /**
33 * Defines the category for the module.
34 *
35 * When modules are listed they are usually grouped by category. The category is also often
36 * used to create the main navigation menu of the admin interface.
37 *
38 * The category of the module is translated within the `module_category` scope.
39 *
40 * @var string
41 */
42 const T_CATEGORY = 'category';
43
44 /**
45 * Defines the PHP class of the module.
46 *
47 * If the class is not defined it is resolved during indexing using the {@link T_NAMESPACE}
48 * tag and the following pattern : `<namespace>\Module`.
49 *
50 * @var string
51 */
52 const T_CLASS = 'class';
53
54 /**
55 * Defines a short description of what the module do.
56 *
57 * @var string
58 */
59 const T_DESCRIPTION = 'description';
60
61 /**
62 * Defines the state of the module.
63 *
64 * @var bool
65 */
66 const T_DISABLED = 'disabled';
67
68 /**
69 * Defines the module that the module extends.
70 *
71 * @var string|\ICanBoogie\Module
72 */
73 const T_EXTENDS = 'extends';
74
75 /**
76 * Defines the identifier of the module.
77 *
78 * If the identifier is not defined the name of the module directory is used instead.
79 *
80 * @var string
81 */
82 const T_ID = 'id';
83
84 /**
85 * Defines the state of the module.
86 *
87 * Required modules are always enabled.
88 *
89 * @var bool
90 */
91 const T_REQUIRED = 'required';
92
93 /**
94 * Defines the modules that the module requires.
95 *
96 * The required modules are defined using an array where each key/value pair is the identifier
97 * of the module and the minimum version required.
98 *
99 * @var array[string]string
100 */
101 const T_REQUIRES = 'requires';
102
103 /**
104 * Defines the models of the module.
105 *
106 * @var array[string]array|string
107 */
108 const T_MODELS = 'models';
109
110 /**
111 * Defines the namespace of the module.
112 *
113 * This attribute must be defined at construct time.
114 *
115 * @var string
116 */
117 const T_NAMESPACE = 'namespace';
118
119 /**
120 * Path to the module's directory.
121 *
122 * This tag is resolved when the module is indexed.
123 *
124 * @var string
125 */
126 const T_PATH = 'path';
127
128 /**
129 * General permission of the module.
130 *
131 * @var string|int
132 */
133 const T_PERMISSION = 'permission';
134
135 /**
136 * Defines the permissions added by the module.
137 *
138 * @var array[]string
139 */
140 const T_PERMISSIONS = 'permissions';
141
142 /**
143 * Defines the title of the module.
144 *
145 * The title of the module is translated within the `module_title` scope.
146 *
147 * @var string
148 */
149 const T_TITLE = 'title';
150
151 /**
152 * Defines the version (and revision) of the module.
153 *
154 * @var string
155 */
156 const T_VERSION = 'version';
157
158 /**
159 * Defines the weight of the module.
160 *
161 * The weight of the module is resolved during modules indexing according to the
162 * {@link T_EXTENDS} and {@link T_REQUIRES} tags.
163 *
164 * @var int
165 */
166 const T_WEIGHT = 'weight';
167
168 /*
169 * PERMISSIONS:
170 *
171 * NONE: Well, you can't do anything
172 *
173 * ACCESS: You can access the module and view its records
174 *
175 * CREATE: You can create new records
176 *
177 * MAINTAIN: You can edit the records you created
178 *
179 * MANAGE: You can delete the records you created
180 *
181 * ADMINISTER: You have complete control over the module
182 *
183 */
184 const PERMISSION_NONE = 0;
185 const PERMISSION_ACCESS = 1;
186 const PERMISSION_CREATE = 2;
187 const PERMISSION_MAINTAIN = 3;
188 const PERMISSION_MANAGE = 4;
189 const PERMISSION_ADMINISTER = 5;
190
191 /**
192 * Defines the name of the operation used to save the records of the module.
193 *
194 * @var string
195 */
196 const OPERATION_SAVE = 'save';
197
198 /**
199 * Defines the name of the operation used to delete the records of the module.
200 *
201 * @var string
202 */
203 const OPERATION_DELETE = 'delete';
204
205 /**
206 * Returns the identifier of the module as defined by its descriptor.
207 *
208 * This method is the getter for the {@link $id} magic property.
209 *
210 * @return string
211 */
212 protected function get_id()
213 {
214 return $this->descriptor[self::T_ID];
215 }
216
217 /**
218 * Returns the path of the module as defined by its descriptor.
219 *
220 * This method is the getter for the {@link $path} magic property.
221 *
222 * @return string
223 */
224 protected function get_path()
225 {
226 return $this->descriptor[self::T_PATH];
227 }
228
229 /**
230 * The descriptor of the module.
231 *
232 * @var array[string]mixed
233 */
234 protected $descriptor;
235
236 /**
237 * Returns the descriptor of the module.
238 *
239 * This method is the getter for the {@link $descriptor} magic property.
240 *
241 * @return array[string]mixed
242 */
243 protected function get_descriptor()
244 {
245 return $this->descriptor;
246 }
247
248 /**
249 * Constructor.
250 *
251 * Initializes the {@link $descriptor} property.
252 *
253 * @param array $descriptor
254 */
255 public function __construct(array $descriptor)
256 {
257 $this->descriptor = $descriptor;
258 }
259
260 /**
261 * Returns the identifier of the module.
262 *
263 * @return string
264 */
265 public function __toString()
266 {
267 return $this->id;
268 }
269
270 /**
271 * Returns the _flat_ version of the module's identifier.
272 *
273 * This method is the getter for the {@link $flat_id} magic property.
274 *
275 * @return string
276 */
277 protected function get_flat_id()
278 {
279 return strtr($this->id, [
280
281 '.' => '_',
282 '-' => '_'
283
284 ]);
285 }
286
287 /**
288 * Returns the primary model of the module.
289 *
290 * This is the getter for the {@link $model} magic property.
291 *
292 * @return ActiveRecord\Model
293 */
294 protected function get_model()
295 {
296 return $this->model();
297 }
298
299 /**
300 * Returns the module title, translated to the current language.
301 *
302 * @return string
303 */
304 protected function get_title()
305 {
306 $default = isset($this->descriptor[self::T_TITLE]) ? $this->descriptor[self::T_TITLE] : 'Undefined';
307
308 return I18n\t($this->flat_id, [], [ 'scope' => 'module_title', 'default' => $default ]);
309 }
310
311 /**
312 * Returns the parent module.
313 *
314 * @return Module|null
315 */
316 protected function get_parent()
317 {
318 global $core;
319
320 $extends = $this->descriptor[self::T_EXTENDS];
321
322 return $extends ? ($extends instanceof self ? $extends : $core->modules[$extends]) : null;
323 }
324
325 /**
326 * Checks if the module is installed.
327 *
328 * @param Errors $errors Error collection.
329 *
330 * @return mixed `true` if the module is installed, `false` if the module
331 * (or parts of) is not installed, `null` if the module has no installation.
332 */
333 public function is_installed(Errors $errors)
334 {
335 if (empty($this->descriptor[self::T_MODELS]))
336 {
337 return null;
338 }
339
340 $rc = true;
341
342 foreach ($this->descriptor[self::T_MODELS] as $name => $tags)
343 {
344 if (!$this->model($name)->is_installed())
345 {
346 $rc = false;
347 }
348 }
349
350 return $rc;
351 }
352
353 /**
354 * Install the module.
355 *
356 * If the module has models they are installed.
357 *
358 * @param Errors $errors Error collection.
359 *
360 * @return boolean|null true if the module has successfully been installed, false if the
361 * module (or parts of the module) fails to install or null if the module has
362 * no installation process.
363 */
364 public function install(Errors $errors)
365 {
366 if (empty($this->descriptor[self::T_MODELS]))
367 {
368 return null;
369 }
370
371 $rc = true;
372
373 foreach ($this->descriptor[self::T_MODELS] as $name => $tags)
374 {
375 $model = $this->model($name);
376
377 if ($model->is_installed())
378 {
379 continue;
380 }
381
382 if (!$model->install())
383 {
384 $errors[$this->id] = t('Unable to install model %model', [ '%model' => $name ]);
385
386 $rc = false;
387 }
388 }
389
390 return $rc;
391 }
392
393 /**
394 * Uninstall the module.
395 *
396 * Basically it uninstall the models installed by the module.
397 *
398 * @return boolean|null `true` if the module was successfully uninstalled. `false` if the module
399 * (or parts of the module) failed to uninstall. `null` if there is no uninstall process.
400 */
401 public function uninstall()
402 {
403 if (empty($this->descriptor[self::T_MODELS]))
404 {
405 return;
406 }
407
408 $rc = true;
409
410 foreach ($this->descriptor[self::T_MODELS] as $name => $tags)
411 {
412 $model = $this->model($name);
413
414 if (!$model->is_installed())
415 {
416 continue;
417 }
418
419 if (!$model->uninstall())
420 {
421 $rc = false;
422 }
423 }
424
425 return $rc;
426 }
427
428 /**
429 * Cache for loaded models.
430 *
431 * @var array[string]ActiveRecord\Model
432 */
433 protected $models = [];
434
435 /**
436 * Get a model from the module.
437 *
438 * If the model has not been created yet, it is created on the fly.
439 *
440 * @param string $which The identifier of the model to get.
441 *
442 * @return Model The requested model.
443 *
444 * @throws Exception if the model does not exists.
445 */
446 public function model($which='primary')
447 {
448 if (empty($this->models[$which]))
449 {
450 if (empty($this->descriptor[self::T_MODELS][$which]))
451 {
452 throw new ModelNotDefined($which);
453 }
454
455 #
456 # resolve model tags
457 #
458
459 $callback = "resolve_{$which}_model_tags";
460
461 if (!method_exists($this, $callback))
462 {
463 $callback = 'resolve_model_tags';
464 }
465
466 $attributes = $this->$callback($this->descriptor[self::T_MODELS][$which], $which);
467
468 #
469 # COMPAT WITH 'inherit'
470 #
471
472 if ($attributes instanceof Model)
473 {
474 $this->models[$which] = $attributes;
475
476 return $attributes;
477 }
478
479 #
480 # create model
481 #
482
483 $class = $attributes[Model::T_CLASS];
484
485 $this->models[$which] = new $class($attributes);
486 }
487
488 #
489 # return cached model
490 #
491
492 return $this->models[$which];
493 }
494
495 protected function resolve_model_tags($tags, $which)
496 {
497 global $core;
498
499 #
500 # The model may use another model, in which case the model to use is defined using a
501 # string e.g. 'contents' or 'terms/nodes'
502 #
503
504 if (is_string($tags))
505 {
506 $model_name = $tags;
507
508 if ($model_name == 'inherit')
509 {
510 $class = get_parent_class($this);
511
512 foreach ($core->modules->descriptors as $module_id => $descriptor)
513 {
514 if ($class != $descriptor['class'])
515 {
516 continue;
517 }
518
519 $model_name = $core->models[$module_id];
520
521 break;
522 }
523 }
524
525 $tags = [ Model::T_EXTENDS => $model_name ];
526 }
527
528 #
529 # defaults
530 #
531
532 $id = $this->id;
533
534 $tags += [
535
536 Model::CONNECTION => 'primary',
537 Model::ID => $which == 'primary' ? $id : $id . '/' . $which
538
539 ];
540
541 if (empty($tags[Model::NAME]))
542 {
543 $tags[Model::NAME] = Modules::format_model_name($id, $which);
544 }
545
546 #
547 # relations
548 #
549
550 if (isset($tags[Model::T_EXTENDS]))
551 {
552 $extends = &$tags[Model::T_EXTENDS];
553
554 if (is_string($extends))
555 {
556 $extends = $core->models[$extends];
557 }
558
559 if (!$tags[Model::T_CLASS])
560 {
561 $tags[Model::T_CLASS] = get_class($extends);
562 }
563 }
564
565 #
566 #
567 #
568
569 if (isset($tags[Model::T_IMPLEMENTS]))
570 {
571 $implements =& $tags[Model::T_IMPLEMENTS];
572
573 foreach ($implements as &$implement)
574 {
575 if (isset($implement['model']))
576 {
577 list($implement_id, $implement_which) = explode('/', $implement['model']) + [ 1 => 'primary' ];
578
579 if ($id == $implement_id && $which == $implement_which)
580 {
581 throw new Exception('Model %module/%model implements itself !', [ '%module' => $id, '%model' => $which ]);
582 }
583
584 $module = ($implement_id == $id) ? $this : $core->modules[$implement_id];
585
586 $implement['table'] = $module->model($implement_which);
587 }
588 else if (is_string($implement['table']))
589 {
590 throw new Exception('Model %model of module %module implements a table: %table', [
591
592 '%model' => $which,
593 '%module' => $id,
594 '%table' => $implement['table']
595
596 ]);
597
598 $implement['table'] = $models[$implement['table']];
599 }
600 }
601 }
602
603 #
604 # default class, if none was defined.
605 #
606
607 if (empty($tags[Model::CLASSNAME]))
608 {
609 $tags[Model::CLASSNAME] = 'ICanBoogie\ActiveRecord\Model';
610 }
611
612 #
613 # connection
614 #
615
616 $connection = $tags[Model::CONNECTION];
617
618 if (!($connection instanceof Connection))
619 {
620 $tags[Model::CONNECTION] = $core->connections[$connection];
621 }
622
623 return $tags;
624 }
625
626 /**
627 * Get a block.
628 *
629 * @param string $name The name of the block to get.
630 *
631 * @return mixed Depends on the implementation. Should return a string or object that can be stringified.
632 *
633 * @throws Exception if the block is not defined.
634 */
635 public function getBlock($name)
636 {
637 $args = func_get_args();
638
639 array_shift($args);
640
641 $method_name = 'handle_block_' . $name;
642
643 if (method_exists($this, $method_name))
644 {
645 array_shift($args);
646
647 return call_user_func_array([ $this, $method_name ], $args);
648 }
649
650 $callback = 'block_' . $name;
651
652 if (!method_exists($this, $callback))
653 {
654 throw new Exception('The %method method is missing from the %module module to create block %type.', [
655
656 '%method' => $callback,
657 '%module' => $this->id,
658 '%type' => $name
659
660 ]);
661 }
662
663 return call_user_func_array([ $this, $callback ], $args);
664 }
665 }