1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the ICanBoogie package. |
5
|
|
|
* |
6
|
|
|
* (c) Olivier Laviale <[email protected]> |
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
|
|
|
use ICanBoogie\Module\Descriptor; |
19
|
|
|
use ICanBoogie\Module\ModuleCollection; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* A module of the framework. |
23
|
|
|
* |
24
|
|
|
* @property-read array $descriptor The descriptor of the module. |
25
|
|
|
* @property-read string $flat_id Underscored identifier. |
26
|
|
|
* @property-read string $id The identifier of the module, defined by {@link Descriptor::ID}. |
27
|
|
|
* @property-read Model $model The primary model of the module. |
28
|
|
|
* @property-read Module $parent The parent module, defined by {@link Descriptor::INHERITS}. |
29
|
|
|
* @property-read string $path The path to the module, defined by {@link Descriptor::PATH}. |
30
|
|
|
* @property-read string $title The localized title of the module. |
31
|
|
|
* @property-read ModuleCollection $collection |
32
|
|
|
* @property-read Core|Module\CoreBindings|Binding\ActiveRecord\CoreBindings|Binding\I18n\CoreBindings $app |
33
|
|
|
*/ |
34
|
|
|
class Module extends Prototyped |
35
|
|
|
{ |
36
|
|
|
/* |
37
|
|
|
* PERMISSIONS: |
38
|
|
|
* |
39
|
|
|
* NONE: Well, you can't do anything |
40
|
|
|
* |
41
|
|
|
* ACCESS: You can access the module and view its records |
42
|
|
|
* |
43
|
|
|
* CREATE: You can create new records |
44
|
|
|
* |
45
|
|
|
* MAINTAIN: You can edit the records you created |
46
|
|
|
* |
47
|
|
|
* MANAGE: You can delete the records you created |
48
|
|
|
* |
49
|
|
|
* ADMINISTER: You have complete control over the module |
50
|
|
|
* |
51
|
|
|
*/ |
52
|
|
|
const PERMISSION_NONE = 0; |
53
|
|
|
const PERMISSION_ACCESS = 1; |
54
|
|
|
const PERMISSION_CREATE = 2; |
55
|
|
|
const PERMISSION_MAINTAIN = 3; |
56
|
|
|
const PERMISSION_MANAGE = 4; |
57
|
|
|
const PERMISSION_ADMINISTER = 5; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Defines the name of the operation used to save the records of the module. |
61
|
|
|
*/ |
62
|
|
|
const OPERATION_SAVE = 'save'; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Defines the name of the operation used to delete the records of the module. |
66
|
|
|
*/ |
67
|
|
|
const OPERATION_DELETE = 'delete'; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Returns the identifier of the module as defined by its descriptor. |
71
|
|
|
* |
72
|
|
|
* This method is the getter for the {@link $id} magic property. |
73
|
|
|
* |
74
|
|
|
* @return string |
75
|
|
|
*/ |
76
|
|
|
protected function get_id() |
77
|
|
|
{ |
78
|
|
|
return $this->descriptor[Descriptor::ID]; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Returns the path of the module as defined by its descriptor. |
83
|
|
|
* |
84
|
|
|
* This method is the getter for the {@link $path} magic property. |
85
|
|
|
* |
86
|
|
|
* @return string |
87
|
|
|
*/ |
88
|
|
|
protected function get_path() |
89
|
|
|
{ |
90
|
|
|
return $this->descriptor[Descriptor::PATH]; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* The descriptor of the module. |
95
|
|
|
* |
96
|
|
|
* @var array |
97
|
|
|
*/ |
98
|
|
|
protected $descriptor; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Returns the descriptor of the module. |
102
|
|
|
* |
103
|
|
|
* This method is the getter for the {@link $descriptor} magic property. |
104
|
|
|
* |
105
|
|
|
* @return array |
106
|
|
|
*/ |
107
|
|
|
protected function get_descriptor() |
108
|
|
|
{ |
109
|
|
|
return $this->descriptor; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Cache for loaded models. |
114
|
|
|
* |
115
|
|
|
* @var ActiveRecord\Model[] |
116
|
|
|
*/ |
117
|
|
|
private $models = []; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @var ModuleCollection |
121
|
|
|
*/ |
122
|
|
|
private $collection; |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @return ModuleCollection |
126
|
|
|
*/ |
127
|
|
|
protected function get_collection() |
128
|
|
|
{ |
129
|
|
|
return $this->collection; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Constructor. |
134
|
|
|
* |
135
|
|
|
* Initializes the {@link $descriptor} property. |
136
|
|
|
* |
137
|
|
|
* @param ModuleCollection $collection |
138
|
|
|
* @param array $descriptor |
139
|
|
|
*/ |
140
|
|
|
public function __construct(ModuleCollection $collection, array $descriptor) |
141
|
|
|
{ |
142
|
|
|
$this->collection = $collection; |
143
|
|
|
$this->descriptor = $descriptor; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Returns the identifier of the module. |
148
|
|
|
* |
149
|
|
|
* @return string |
150
|
|
|
*/ |
151
|
|
|
public function __toString() |
152
|
|
|
{ |
153
|
|
|
return $this->id; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Returns the _flat_ version of the module's identifier. |
158
|
|
|
* |
159
|
|
|
* This method is the getter for the {@link $flat_id} magic property. |
160
|
|
|
* |
161
|
|
|
* @return string |
162
|
|
|
*/ |
163
|
|
|
protected function get_flat_id() |
164
|
|
|
{ |
165
|
|
|
return strtr($this->id, [ |
166
|
|
|
|
167
|
|
|
'.' => '_', |
168
|
|
|
'-' => '_' |
169
|
|
|
|
170
|
|
|
]); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Returns the primary model of the module. |
175
|
|
|
* |
176
|
|
|
* This is the getter for the {@link $model} magic property. |
177
|
|
|
* |
178
|
|
|
* @return ActiveRecord\Model |
179
|
|
|
*/ |
180
|
|
|
protected function get_model() |
181
|
|
|
{ |
182
|
|
|
return $this->model(); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Returns the module title, translated to the current language. |
187
|
|
|
* |
188
|
|
|
* @return string |
189
|
|
|
* |
190
|
|
|
* @deprecated |
191
|
|
|
*/ |
192
|
|
|
protected function get_title() |
193
|
|
|
{ |
194
|
|
|
$default = isset($this->descriptor[Descriptor::TITLE]) ? $this->descriptor[Descriptor::TITLE] : 'Undefined'; |
195
|
|
|
|
196
|
|
|
return $this->app->translate($this->flat_id, [], [ 'scope' => 'module_title', 'default' => $default ]); |
|
|
|
|
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Returns the parent module. |
201
|
|
|
* |
202
|
|
|
* @return Module|null |
203
|
|
|
*/ |
204
|
|
|
protected function get_parent() |
205
|
|
|
{ |
206
|
|
|
return $this->descriptor[Descriptor::INHERITS]; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Checks if the module is installed. |
211
|
|
|
* |
212
|
|
|
* @param ErrorCollection $errors Error collection. |
213
|
|
|
* |
214
|
|
|
* @return mixed `true` if the module is installed, `false` if the module |
215
|
|
|
* (or parts of) is not installed, `null` if the module has no installation. |
216
|
|
|
*/ |
217
|
|
|
public function is_installed(ErrorCollection $errors) |
218
|
|
|
{ |
219
|
|
|
if (empty($this->descriptor[Descriptor::MODELS])) |
220
|
|
|
{ |
221
|
|
|
return null; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
$rc = true; |
225
|
|
|
|
226
|
|
|
foreach ($this->descriptor[Descriptor::MODELS] as $name => $tags) |
227
|
|
|
{ |
228
|
|
|
if (!$this->model($name)->is_installed()) |
229
|
|
|
{ |
230
|
|
|
$errors->add($this->id, "The model %name is not installed.", [ |
231
|
|
|
|
232
|
|
|
'name' => $name |
233
|
|
|
|
234
|
|
|
]); |
235
|
|
|
|
236
|
|
|
$rc = false; |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
return $rc; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Install the module. |
245
|
|
|
* |
246
|
|
|
* If the module has models they are installed. |
247
|
|
|
* |
248
|
|
|
* @param ErrorCollection $errors Error collection. |
249
|
|
|
* |
250
|
|
|
* @return boolean|null true if the module has successfully been installed, false if the |
251
|
|
|
* module (or parts of the module) fails to install or null if the module has |
252
|
|
|
* no installation process. |
253
|
|
|
*/ |
254
|
|
|
public function install(ErrorCollection $errors) |
255
|
|
|
{ |
256
|
|
|
if (empty($this->descriptor[Descriptor::MODELS])) |
257
|
|
|
{ |
258
|
|
|
return null; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
$rc = true; |
262
|
|
|
|
263
|
|
|
foreach ($this->descriptor[Descriptor::MODELS] as $name => $tags) |
264
|
|
|
{ |
265
|
|
|
$model = $this->model($name); |
266
|
|
|
|
267
|
|
|
if ($model->is_installed()) |
268
|
|
|
{ |
269
|
|
|
continue; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
try |
273
|
|
|
{ |
274
|
|
|
$model->install(); |
275
|
|
|
} |
276
|
|
|
catch (\Exception $e) |
277
|
|
|
{ |
278
|
|
|
$errors->add($this->id, "Unable to install model %model: !message", [ |
279
|
|
|
|
280
|
|
|
'model' => $name, |
281
|
|
|
'message' => $e->getMessage() |
282
|
|
|
|
283
|
|
|
]); |
284
|
|
|
|
285
|
|
|
$rc = false; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
return $rc; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Uninstall the module. |
294
|
|
|
* |
295
|
|
|
* Basically it uninstall the models installed by the module. |
296
|
|
|
* |
297
|
|
|
* @return boolean|null `true` if the module was successfully uninstalled. `false` if the module |
298
|
|
|
* (or parts of the module) failed to uninstall. `null` if there is no uninstall process. |
299
|
|
|
*/ |
300
|
|
|
public function uninstall() |
301
|
|
|
{ |
302
|
|
|
if (empty($this->descriptor[Descriptor::MODELS])) |
303
|
|
|
{ |
304
|
|
|
return null; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
$rc = true; |
308
|
|
|
|
309
|
|
|
foreach ($this->descriptor[Descriptor::MODELS] as $name => $tags) |
310
|
|
|
{ |
311
|
|
|
$model = $this->model($name); |
312
|
|
|
|
313
|
|
|
if (!$model->is_installed()) |
314
|
|
|
{ |
315
|
|
|
continue; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
if (!$model->uninstall()) |
319
|
|
|
{ |
320
|
|
|
$rc = false; |
321
|
|
|
} |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
return $rc; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Get a model from the module. |
329
|
|
|
* |
330
|
|
|
* If the model has not been created yet, it is created on the fly. |
331
|
|
|
* |
332
|
|
|
* @param string $which The identifier of the model to get. |
333
|
|
|
* |
334
|
|
|
* @return Model The requested model. |
335
|
|
|
* |
336
|
|
|
* @throws ModelNotDefined when the model is not defined by the module. |
337
|
|
|
* @throws \RuntimeException when the class of the model does not exists. |
338
|
|
|
*/ |
339
|
|
|
public function model($which = 'primary') |
340
|
|
|
{ |
341
|
|
|
if (empty($this->models[$which])) |
342
|
|
|
{ |
343
|
|
|
if (empty($this->descriptor[Descriptor::MODELS][$which])) |
344
|
|
|
{ |
345
|
|
|
throw new ModelNotDefined($which); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
# |
349
|
|
|
# resolve model tags |
350
|
|
|
# |
351
|
|
|
|
352
|
|
|
$callback = "resolve_{$which}_model_tags"; |
353
|
|
|
|
354
|
|
|
if (!method_exists($this, $callback)) |
355
|
|
|
{ |
356
|
|
|
$callback = 'resolve_model_tags'; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
$attributes = $this->$callback($this->descriptor[Descriptor::MODELS][$which], $which); |
360
|
|
|
|
361
|
|
|
# |
362
|
|
|
# COMPATIBILITY WITH 'inherit' |
363
|
|
|
# |
364
|
|
|
|
365
|
|
|
if ($attributes instanceof Model) |
366
|
|
|
{ |
367
|
|
|
$this->models[$which] = $attributes; |
368
|
|
|
|
369
|
|
|
return $attributes; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
# |
373
|
|
|
# create model |
374
|
|
|
# |
375
|
|
|
|
376
|
|
|
$class = $attributes[Model::CLASSNAME]; |
377
|
|
|
|
378
|
|
|
if (!class_exists($class)) |
379
|
|
|
{ |
380
|
|
|
throw new \RuntimeException(\ICanBoogie\format("Unable to instantiate model %model, the class %class does not exists.", [ |
381
|
|
|
|
382
|
|
|
'model' => "$this->id/$which", |
383
|
|
|
'class' => $class |
384
|
|
|
|
385
|
|
|
])); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
$this->models[$which] = new $class($this->app->models, $attributes); |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
# |
392
|
|
|
# return cached model |
393
|
|
|
# |
394
|
|
|
|
395
|
|
|
return $this->models[$which]; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Resolves model tags. |
400
|
|
|
* |
401
|
|
|
* @param array|string $tags |
402
|
|
|
* @param string $which |
403
|
|
|
* |
404
|
|
|
* @return array |
405
|
|
|
*/ |
406
|
|
|
protected function resolve_model_tags($tags, $which) |
407
|
|
|
{ |
408
|
|
|
$app = $this->app; |
409
|
|
|
|
410
|
|
|
# |
411
|
|
|
# The model may use another model, in which case the model to use is defined using a |
412
|
|
|
# string e.g. 'contents' or 'terms/nodes' |
413
|
|
|
# |
414
|
|
|
|
415
|
|
|
if (is_string($tags)) |
416
|
|
|
{ |
417
|
|
|
$model_name = $tags; |
418
|
|
|
|
419
|
|
|
if ($model_name == 'inherit') |
420
|
|
|
{ |
421
|
|
|
$class = get_parent_class($this); |
422
|
|
|
|
423
|
|
|
foreach ($app->modules->descriptors as $module_id => $descriptor) |
424
|
|
|
{ |
425
|
|
|
if ($class != $descriptor['class']) |
426
|
|
|
{ |
427
|
|
|
continue; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
$model_name = $app->models[$module_id]; |
431
|
|
|
|
432
|
|
|
break; |
433
|
|
|
} |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
$tags = [ Model::EXTENDING => $model_name ]; |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
# |
440
|
|
|
# defaults |
441
|
|
|
# |
442
|
|
|
|
443
|
|
|
$id = $this->id; |
444
|
|
|
|
445
|
|
|
$tags += [ |
446
|
|
|
|
447
|
|
|
Model::CONNECTION => 'primary', |
448
|
|
|
Model::ID => $which == 'primary' ? $id : $id . '/' . $which, |
449
|
|
|
Model::EXTENDING => null |
450
|
|
|
|
451
|
|
|
]; |
452
|
|
|
|
453
|
|
|
if (empty($tags[Model::NAME])) |
454
|
|
|
{ |
455
|
|
|
$tags[Model::NAME] = ModuleCollection::format_model_name($id, $which); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
# |
459
|
|
|
# relations |
460
|
|
|
# |
461
|
|
|
|
462
|
|
|
if (isset($tags[Model::EXTENDING])) |
463
|
|
|
{ |
464
|
|
|
$extends = &$tags[Model::EXTENDING]; |
465
|
|
|
|
466
|
|
|
if (is_string($extends)) |
467
|
|
|
{ |
468
|
|
|
$extends = $this->app->models[$extends]; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
if (!$tags[Model::CLASSNAME]) |
472
|
|
|
{ |
473
|
|
|
$tags[Model::CLASSNAME] = get_class($extends); |
474
|
|
|
} |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
# |
478
|
|
|
# |
479
|
|
|
# |
480
|
|
|
|
481
|
|
|
if (isset($tags[Model::IMPLEMENTING])) |
482
|
|
|
{ |
483
|
|
|
$implements =& $tags[Model::IMPLEMENTING]; |
484
|
|
|
|
485
|
|
|
foreach ($implements as &$implement) |
486
|
|
|
{ |
487
|
|
|
if (isset($implement['model'])) |
488
|
|
|
{ |
489
|
|
|
list($implement_id, $implement_which) = explode('/', $implement['model']) + [ 1 => 'primary' ]; |
490
|
|
|
|
491
|
|
|
if ($id == $implement_id && $which == $implement_which) |
492
|
|
|
{ |
493
|
|
|
throw new \RuntimeException(\ICanBoogie\format('Model %module/%model implements itself !', [ |
494
|
|
|
|
495
|
|
|
'%module' => $id, |
496
|
|
|
'%model' => $which |
497
|
|
|
|
498
|
|
|
])); |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
$module = ($implement_id == $id) ? $this : \ICanBoogie\app()->modules[$implement_id]; |
502
|
|
|
|
503
|
|
|
$implement['table'] = $module->model($implement_which); |
504
|
|
|
} |
505
|
|
|
else if (is_string($implement['table'])) |
506
|
|
|
{ |
507
|
|
|
throw new \RuntimeException(\ICanBoogie\format('Model %model of module %module implements a table: %table', [ |
508
|
|
|
|
509
|
|
|
'%model' => $which, |
510
|
|
|
'%module' => $id, |
511
|
|
|
'%table' => $implement['table'] |
512
|
|
|
|
513
|
|
|
])); |
514
|
|
|
} |
515
|
|
|
} |
516
|
|
|
} |
517
|
|
|
|
518
|
|
|
# |
519
|
|
|
# default class, if none was defined. |
520
|
|
|
# |
521
|
|
|
|
522
|
|
|
if (empty($tags[Model::CLASSNAME])) |
523
|
|
|
{ |
524
|
|
|
$tags[Model::CLASSNAME] = 'ICanBoogie\ActiveRecord\Model'; |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
# |
528
|
|
|
# connection |
529
|
|
|
# |
530
|
|
|
|
531
|
|
|
$connection = $tags[Model::CONNECTION]; |
532
|
|
|
|
533
|
|
|
if (!($connection instanceof Connection)) |
534
|
|
|
{ |
535
|
|
|
$tags[Model::CONNECTION] = $this->app->connections[$connection]; |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
return $tags; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Get a block. |
543
|
|
|
* |
544
|
|
|
* @param string $name The name of the block to get. |
545
|
|
|
* |
546
|
|
|
* @return mixed Depends on the implementation. Should return a string or an object |
547
|
|
|
* implementing `__toString`. |
548
|
|
|
* |
549
|
|
|
* @throws \RuntimeException if the block is not defined. |
550
|
|
|
*/ |
551
|
|
|
public function getBlock($name) |
552
|
|
|
{ |
553
|
|
|
$args = func_get_args(); |
554
|
|
|
|
555
|
|
|
array_shift($args); |
556
|
|
|
|
557
|
|
|
$callback = 'block_' . $name; |
558
|
|
|
|
559
|
|
|
if (!method_exists($this, $callback)) |
560
|
|
|
{ |
561
|
|
|
throw new \RuntimeException(\ICanBoogie\format('The %method method is missing from the %module module to create block %type.', [ |
562
|
|
|
|
563
|
|
|
'%method' => $callback, |
564
|
|
|
'%module' => $this->id, |
565
|
|
|
'%type' => $name |
566
|
|
|
|
567
|
|
|
])); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
return call_user_func_array([ $this, $callback ], $args); |
571
|
|
|
} |
572
|
|
|
} |
573
|
|
|
|
It seems like the method you are trying to call exists only in some of the possible types.
Let’s take a look at an example:
Available Fixes
Add an additional type-check:
Only allow a single type to be passed if the variable comes from a parameter: