1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie;
13
14 use ICanBoogie\ActiveRecord\Model;
15
16 17 18 19 20 21 22 23 24
25 class Modules extends Object implements \ArrayAccess, \IteratorAggregate
26 {
27 28 29 30 31 32 33 34
35 static public function format_model_name($module_id, $model_id='primary')
36 {
37 return preg_replace('#[^0-9,a-z,A-Z$_]#', '_', $module_id) . ($model_id == 'primary' ? '' : '__' . $model_id);
38 }
39
40 41 42 43 44
45 public $descriptors = [];
46
47 48 49 50 51
52 protected $paths = [];
53
54 55 56 57 58
59 protected $cache;
60
61 62 63 64 65
66 protected $modules = [];
67
68 69 70 71 72 73
74 public function __construct($paths, Vars $cache=null)
75 {
76 $this->paths = $paths;
77 $this->cache = $cache;
78 }
79
80 81 82 83 84 85 86 87 88 89 90 91 92
93 protected function revoke_constructions()
94 {
95 unset($this->enabled_modules_descriptors);
96 unset($this->disabled_modules_descriptors);
97 unset($this->catalog_paths);
98 unset($this->config_paths);
99 }
100
101 102 103 104 105
106 public function enable($id)
107 {
108 $this->index;
109
110 if (empty($this->descriptors[$id]))
111 {
112 return;
113 }
114
115 $this->descriptors[$id][Module::T_DISABLED] = false;
116 $this->revoke_constructions();
117 }
118
119 120 121 122 123
124 public function disable($id)
125 {
126 $this->index;
127
128 if (empty($this->descriptors[$id]))
129 {
130 return;
131 }
132
133 $this->descriptors[$id][Module::T_DISABLED] = true;
134 $this->revoke_constructions();
135 }
136
137 138 139 140 141 142 143 144 145
146 public function offsetSet($id, $enable)
147 {
148 if (empty($this->descriptors[$id]))
149 {
150 return;
151 }
152
153 $this->descriptors[$id][Module::T_DISABLED] = empty($enable);
154 $this->revoke_constructions();
155 }
156
157 158 159 160 161 162 163 164 165 166 167 168 169 170
171 public function offsetExists($id)
172 {
173 $descriptors = $this->descriptors;
174
175 return (isset($descriptors[$id]) && empty($descriptors[$id][Module::T_DISABLED]));
176 }
177
178 179 180 181 182 183 184 185
186 public function offsetUnset($id)
187 {
188 if (empty($this->descriptors[$id]))
189 {
190 return;
191 }
192
193 $this->descriptors[$id][Module::T_DISABLED] = true;
194 $this->revoke_constructions();
195 }
196
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
213 public function offsetGet($id)
214 {
215 if (isset($this->modules[$id]))
216 {
217 return $this->modules[$id];
218 }
219
220 $descriptors = $this->descriptors;
221
222 if (empty($descriptors[$id]))
223 {
224 throw new ModuleNotDefined($id);
225 }
226
227 $descriptor = $descriptors[$id];
228
229 if (!empty($descriptor[Module::T_DISABLED]))
230 {
231 throw new ModuleIsDisabled($id);
232 }
233
234 $class = $descriptor[Module::T_CLASS];
235
236 if (!class_exists($class, true))
237 {
238 throw new ModuleConstructorMissing($id, $class);
239 }
240
241 return $this->modules[$id] = new $class($descriptor);
242 }
243
244 245 246 247 248
249 public function getIterator()
250 {
251 return new \ArrayIterator($this->modules);
252 }
253
254 255 256 257 258 259 260 261 262 263 264
265 protected function lazy_get_index()
266 {
267 if ($this->cache)
268 {
269 $key = 'cached_modules_' . md5(implode('#', $this->paths));
270 $index = $this->cache[$key];
271
272 if (!$index)
273 {
274 $this->cache[$key] = $index = $this->index_construct();
275 }
276 }
277 else
278 {
279 $index = $this->index_construct();
280 }
281
282 $this->descriptors = $index['descriptors'];
283
284 foreach ($this->descriptors as $descriptor)
285 {
286 $namespace = $descriptor[Module::T_NAMESPACE];
287 $constant = $namespace . '\DIR';
288
289 if (!defined($constant))
290 {
291 define($constant, $descriptor[Module::T_PATH]);
292 }
293 }
294
295 return $index;
296 }
297
298 299 300 301 302 303 304 305 306 307 308 309 310
311 protected function index_construct()
312 {
313 $isolated_require = function ($__file__, $__exposed__)
314 {
315 extract($__exposed__);
316
317 return require $__file__;
318 };
319
320 $descriptors = $this->paths ? $this->index_descriptors($this->paths) : [];
321 $catalogs = [];
322 $configs = [];
323
324 foreach ($descriptors as $id => $descriptor)
325 {
326 $path = $descriptor[Module::T_PATH];
327
328 if ($descriptor['__has_locale'])
329 {
330 $catalogs[] = $path . 'locale';
331 }
332
333 if ($descriptor['__has_config'])
334 {
335 $configs[] = $path . 'config';
336 }
337 }
338
339 return [
340
341 'descriptors' => $descriptors,
342 'catalogs' => $catalogs,
343 'configs' => $configs
344
345 ];
346 }
347
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
376 protected function index_descriptors(array $paths)
377 {
378 $descriptors = $this->collect_descriptors($paths);
379
380 if (!$descriptors)
381 {
382 return [];
383 }
384
385
386
387
388
389 $find_parents = function($id, &$parents=[]) use (&$find_parents, &$descriptors)
390 {
391 $parent = $descriptors[$id][Module::T_EXTENDS];
392
393 if ($parent)
394 {
395 $parents[] = $parent;
396
397 $find_parents($parent, $parents);
398 }
399
400 return $parents;
401 };
402
403 foreach ($descriptors as $id => &$descriptor)
404 {
405 $descriptor['__parents'] = $find_parents($id);
406 }
407
408
409
410
411
412 $ordered_ids = $this->order_ids(array_keys($descriptors), $descriptors);
413 $descriptors = array_merge(array_combine($ordered_ids, $ordered_ids), $descriptors);
414
415 foreach ($descriptors as $id => &$descriptor)
416 {
417 foreach ($descriptor[Module::T_MODELS] as $model_id => &$model_descriptor)
418 {
419 if ($model_descriptor == 'inherit')
420 {
421 $parent_descriptor = $descriptors[$descriptor[Module::T_EXTENDS]];
422 $model_descriptor = $parent_descriptor[Module::T_MODELS][$model_id];
423 }
424 }
425
426 $descriptor = $this->alter_descriptor($descriptor);
427 }
428
429 return $descriptors;
430 }
431
432 protected function collect_descriptors(array $paths)
433 {
434 $descriptors = [];
435
436 foreach ($paths as $root)
437 {
438 $root = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
439 $descriptor_path = $root . 'descriptor.php';
440
441 if (file_exists($descriptor_path))
442 {
443 $id = basename(realpath($root));
444 $descriptor = $this->read_descriptor($id, $root);
445
446 $descriptors[$descriptor[Module::T_ID]] = $descriptor;
447 }
448 else
449 {
450 try
451 {
452 $dir = new \DirectoryIterator($root);
453 }
454 catch (\Exception $e)
455 {
456 throw new Exception('Unable to open directory %root', [ 'root' => $root ]);
457 }
458
459 foreach ($dir as $file)
460 {
461 if ($file->isDot() || !$file->isDir())
462 {
463 continue;
464 }
465
466 $id = $file->getFilename();
467 $path = $root . $id . DIRECTORY_SEPARATOR;
468 $descriptor = $this->read_descriptor($id, $path);
469
470 $descriptors[$descriptor[Module::T_ID]] = $descriptor;
471 }
472 }
473 }
474
475 return $descriptors;
476 }
477
478 479 480 481 482 483 484 485 486 487 488 489 490 491
492 protected function read_descriptor($id, $path)
493 {
494 $descriptor_path = $path . 'descriptor.php';
495 $descriptor = require $descriptor_path;
496
497 if (!is_array($descriptor))
498 {
499 throw new \InvalidArgumentException(format
500 (
501 '%var should be an array: %type given instead in %path', [
502
503 'var' => 'descriptor',
504 'type' => gettype($descriptor),
505 'path' => strip_root($descriptor_path)
506
507 ]
508 ));
509 }
510
511 if (empty($descriptor[Module::T_TITLE]))
512 {
513 throw new \InvalidArgumentException(format
514 (
515 'The %name value of the %id module descriptor is empty in %path.', [
516
517 'name' => Module::T_TITLE,
518 'id' => $id,
519 'path' => strip_root($descriptor_path)
520
521 ]
522 ));
523 }
524
525 if (empty($descriptor[Module::T_NAMESPACE]))
526 {
527 throw new \InvalidArgumentException(format
528 (
529 '%name is required. Invalid descriptor for module %id in %path.', [
530
531 'name' => Module::T_NAMESPACE,
532 'id' => $id,
533 'path' => strip_root($descriptor_path)
534
535 ]
536 ));
537 }
538
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
554
555 return $descriptor + [
556
557 Module::T_CATEGORY => null,
558 Module::T_CLASS => $descriptor[Module::T_NAMESPACE] . '\Module',
559 Module::T_DESCRIPTION => null,
560 Module::T_DISABLED => false,
561 Module::T_EXTENDS => null,
562 Module::T_ID => $id,
563 Module::T_MODELS => [],
564 Module::T_PATH => $path,
565 Module::T_PERMISSION => null,
566 Module::T_PERMISSIONS => [],
567 Module::T_REQUIRED => false,
568 Module::T_REQUIRES => [],
569 Module::T_VERSION => 'dev',
570 Module::T_WEIGHT => 0,
571
572 '__has_config' => is_dir($path . 'config'),
573 '__has_locale' => is_dir($path . 'locale'),
574 '__parents' => []
575
576 ];
577 }
578
579 580 581 582 583 584 585
586 protected function alter_descriptor(array $descriptor)
587 {
588 $id = $descriptor[Module::T_ID];
589 $path = $descriptor[Module::T_PATH];
590 $namespace = $descriptor[Module::T_NAMESPACE];
591
592
593
594 foreach ($descriptor[Module::T_MODELS] as $model_id => &$definition)
595 {
596 if (!is_array($definition))
597 {
598 throw new \InvalidArgumentException(format
599 (
600 'Model definition must be array, given: %value.', [ 'value' => $definition ]
601 ));
602 }
603
604 $basename = $id;
605 $separator_position = strrpos($basename, '.');
606
607 if ($separator_position)
608 {
609 $basename = substr($basename, $separator_position + 1);
610 }
611
612 if (empty($definition[Model::NAME]))
613 {
614 $definition[Model::NAME] = self::format_model_name($id, $model_id);
615 }
616
617 if (empty($definition[Model::CLASSNAME]))
618 {
619 $definition[Model::CLASSNAME] = $namespace . '\\' . ($model_id == 'primary' ? 'Model' : camelize(singularize($model_id)) . 'Model');
620 }
621
622 if (empty($definition[Model::ACTIVERECORD_CLASS]))
623 {
624 $definition[Model::ACTIVERECORD_CLASS] = $namespace . '\\' . camelize(singularize($model_id == 'primary' ? $basename : $model_id));
625 }
626 }
627
628 return $descriptor;
629 }
630
631 632 633 634 635 636
637 private function sort_modules_descriptors()
638 {
639 $enabled = [];
640 $disabled = [];
641
642 $this->index;
643
644 foreach ($this->descriptors as $id => &$descriptor)
645 {
646 if (isset($this[$id]))
647 {
648 $enabled[$id] = $descriptor;
649 }
650 else
651 {
652 $disabled[$id] = $descriptor;
653 }
654 }
655
656 $this->enabled_modules_descriptors = $enabled;
657 $this->disabled_modules_descriptors = $disabled;
658 }
659
660 661 662 663 664 665 666
667 protected function lazy_get_disabled_modules_descriptors()
668 {
669 $this->sort_modules_descriptors();
670
671 return $this->disabled_modules_descriptors;
672 }
673
674 675 676 677 678 679 680
681 protected function lazy_get_enabled_modules_descriptors()
682 {
683 $this->sort_modules_descriptors();
684
685 return $this->enabled_modules_descriptors;
686 }
687
688 689 690 691 692
693 protected function lazy_get_locale_paths()
694 {
695 $paths = [];
696
697 foreach ($this->enabled_modules_descriptors as $module_id => $descriptor)
698 {
699 if (!$descriptor['__has_locale'])
700 {
701 continue;
702 }
703
704 $paths[] = $descriptor[Module::T_PATH] . 'locale';
705 }
706
707 return $paths;
708 }
709
710 711 712 713 714
715 protected function lazy_get_config_paths()
716 {
717 $paths = [];
718
719 foreach ($this->enabled_modules_descriptors as $module_id => $descriptor)
720 {
721 if (!$descriptor['__has_config'])
722 {
723 continue;
724 }
725
726 $paths[] = $descriptor[Module::T_PATH] . 'config';
727 }
728
729 return $paths;
730 }
731
732 733 734 735 736 737 738 739
740 public function order_ids(array $ids, array $descriptors=null)
741 {
742 $ordered = [];
743 $extends_weight = [];
744
745 if ($descriptors === null)
746 {
747 $descriptors = $this->descriptors;
748 }
749
750 $count_extends = function($super_id) use (&$count_extends, &$descriptors)
751 {
752 $i = 0;
753
754 foreach ($descriptors as $id => $descriptor)
755 {
756 if ($descriptor[Module::T_EXTENDS] !== $super_id)
757 {
758 continue;
759 }
760
761 $i += 1 + $count_extends($id);
762 }
763
764 return $i;
765 };
766
767 $count_required = function($required_id) use (&$descriptors, &$extends_weight)
768 {
769 $i = 0;
770
771 foreach ($descriptors as $id => $descriptor)
772 {
773 if (empty($descriptor[Module::T_REQUIRES][$required_id]))
774 {
775 continue;
776 }
777
778 $i += 1 + $extends_weight[$id];
779 }
780
781 return $i;
782 };
783
784 foreach ($ids as $id)
785 {
786 $extends_weight[$id] = $count_extends($id);
787 }
788
789 foreach ($ids as $id)
790 {
791 $ordered[$id] = -$extends_weight[$id] -$count_required($id) + $descriptors[$id][Module::T_WEIGHT];
792 }
793
794 stable_sort($ordered);
795
796 return array_keys($ordered);
797 }
798
799 800 801 802 803 804 805 806 807
808 public function usage($module_id, $all=false)
809 {
810 $n = 0;
811
812 foreach ($this->descriptors as $m_id => $descriptor)
813 {
814 if (!$all && !isset($this[$m_id]))
815 {
816 continue;
817 }
818
819 if ($descriptor[Module::T_EXTENDS] == $module_id)
820 {
821 $n++;
822 }
823
824 if (!empty($descriptor[Module::T_REQUIRES][$module_id]))
825 {
826 $n++;
827 }
828 }
829
830 return $n;
831 }
832
833 834 835 836 837 838 839 840
841 public function is_extending($module_id, $extending_id)
842 {
843 while ($module_id)
844 {
845 if ($module_id == $extending_id)
846 {
847 return true;
848 }
849
850 $descriptor = $this->descriptors[$module_id];
851
852 $module_id = isset($descriptor[Module::T_EXTENDS]) ? $descriptor[Module::T_EXTENDS] : null;
853 }
854
855 return false;
856 }
857 }
858
859 860 861
862
863 864 865 866 867
868 class ModuleIsDisabled extends \RuntimeException
869 {
870 private $module_id;
871
872 public function __construct($module_id, $code=500, \Exception $previous=null)
873 {
874 $this->module_id = $module_id;
875
876 parent::__construct(format('Module is disabled: %module_id', [ 'module_id' => $module_id ]), $code, $previous);
877 }
878
879 public function __get($property)
880 {
881 if ($property == 'module_id')
882 {
883 return $this->module_id;
884 }
885
886 throw new PropertyNotDefined([ $property, $this ]);
887 }
888 }
889
890 891 892 893 894
895 class ModuleNotDefined extends \RuntimeException
896 {
897 898 899 900 901
902 private $module_id;
903
904 public function __construct($module_id, $code=500, \Exception $previous=null)
905 {
906 $this->module_id = $module_id;
907
908 parent::__construct(format('Module is not defined: %module_id', [ 'module_id' => $module_id ]), $code, $previous);
909 }
910
911 public function __get($property)
912 {
913 if ($property == 'module_id')
914 {
915 return $this->module_id;
916 }
917
918 throw new PropertyNotDefined([ $property, $this ]);
919 }
920 }
921
922 923 924 925 926 927
928 class ModuleConstructorMissing extends \RuntimeException
929 {
930 931 932 933 934
935 private $module_id;
936
937 938 939 940 941
942 private $class;
943
944 public function __construct($module_id, $class, $code=500, \Exception $previous=null)
945 {
946 $this->module_id = $module_id;
947 $this->class = $class;
948
949 parent::__construct(format('Missing class %class to instantiate module %id.', [ 'class' => $class, 'id' => $module_id ]));
950 }
951
952 public function __get($property)
953 {
954 if ($property == 'module_id')
955 {
956 return $this->module_id;
957 }
958 else if ($property == 'class')
959 {
960 return $this->class;
961 }
962
963 throw new PropertyNotDefined([ $property, $this ]);
964 }
965 }