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 /**
15 * Subclasses of the {@link Object} class are associated with a prototype, which can be used to
16 * add methods as well as getters and setters to classes.
17 *
18 * When using the ICanBoogie framework, methods can be defined using the "hooks" config and the
19 * "prototypes" namespace:
20 *
21 * <pre>
22 * <?php
23 *
24 * return [
25 *
26 * 'prototypes' => [
27 *
28 * 'Icybee\Modules\Pages\Page::my_additional_method' => 'MyHookClass::my_additional_method',
29 * 'Icybee\Modules\Pages\Page::lazy_get_my_property' => 'MyHookClass::lazy_get_my_property'
30 *
31 * ]
32 * ];
33 * </pre>
34 */
35 class Prototype implements \ArrayAccess, \IteratorAggregate
36 {
37 /**
38 * Prototypes built per class.
39 *
40 * @var array[string]Prototype
41 */
42 static protected $prototypes = [];
43
44 /**
45 * Pool of prototype methods per class.
46 *
47 * @var array[string]callable
48 */
49 static protected $pool;
50
51 /**
52 * Returns the prototype associated with the specified class or object.
53 *
54 * @param string|object $class Class name or instance.
55 *
56 * @return Prototype
57 */
58 static public function from($class)
59 {
60 if (is_object($class))
61 {
62 $class = get_class($class);
63 }
64
65 if (empty(self::$prototypes[$class]))
66 {
67 self::$prototypes[$class] = new static($class);
68 }
69
70 return self::$prototypes[$class];
71 }
72
73 static public function configure(array $config)
74 {
75 self::$pool = $config;
76
77 foreach (self::$prototypes as $class => $prototype)
78 {
79 $prototype->consolidated_methods = null;
80
81 if (empty($config[$class]))
82 {
83 continue;
84 }
85
86 $prototype->methods = $config[$class];
87 }
88 }
89
90 /**
91 * Class associated with the prototype.
92 *
93 * @var string
94 */
95 protected $class;
96
97 /**
98 * Parent prototype.
99 *
100 * @var Prototype
101 */
102 protected $parent;
103
104 /**
105 * Methods defined by the prototype.
106 *
107 * @var array[string]callable
108 */
109 protected $methods = [];
110
111 /**
112 * Methods defined by the prototypes chain.
113 *
114 * @var array[string]callable
115 */
116 protected $consolidated_methods;
117
118 /**
119 * Creates a prototype for the specified class.
120 *
121 * @param string $class
122 */
123 protected function __construct($class)
124 {
125 $this->class = $class;
126
127 $parent_class = get_parent_class($class);
128
129 if ($parent_class)
130 {
131 $this->parent = static::from($parent_class);
132 }
133
134 if (isset(self::$pool[$class]))
135 {
136 $this->methods = self::$pool[$class];
137 }
138 }
139
140 /**
141 * Consolidate the methods of the prototype.
142 *
143 * The method creates a single array from the prototype methods and those of its parents.
144 *
145 * @return array[string]callable
146 */
147 protected function get_consolidated_methods()
148 {
149 if ($this->consolidated_methods !== null)
150 {
151 return $this->consolidated_methods;
152 }
153
154 $methods = $this->methods;
155
156 if ($this->parent)
157 {
158 $methods += $this->parent->get_consolidated_methods();
159 }
160
161 return $this->consolidated_methods = $methods;
162 }
163
164 /**
165 * Revokes the consolidated methods of the prototype.
166 *
167 * The method must be invoked when prototype methods are modified.
168 */
169 protected function revoke_consolidated_methods()
170 {
171 $class = $this->class;
172
173 foreach (self::$prototypes as $prototype)
174 {
175 if (!is_subclass_of($prototype->class, $class))
176 {
177 continue;
178 }
179
180 $prototype->consolidated_methods = null;
181 }
182
183 $this->consolidated_methods = null;
184 }
185
186 /**
187 * Adds or replaces the specified method of the prototype.
188 *
189 * @param string $method The name of the method.
190 *
191 * @param callable $callback
192 */
193 public function offsetSet($method, $callback)
194 {
195 self::$prototypes[$this->class]->methods[$method] = $callback;
196
197 $this->revoke_consolidated_methods();
198 }
199
200 /**
201 * Removed the specified method from the prototype.
202 *
203 * @param string $method The name of the method.
204 */
205 public function offsetUnset($method)
206 {
207 unset(self::$prototypes[$this->class]->methods[$method]);
208
209 $this->revoke_consolidated_methods();
210 }
211
212 /**
213 * Checks if the prototype defines the specified method.
214 *
215 * @param string $method The name of the method.
216 *
217 * @return bool
218 */
219 public function offsetExists($method)
220 {
221 $methods = $this->get_consolidated_methods();
222
223 return isset($methods[$method]);
224 }
225
226 /**
227 * Returns the callback associated with the specified method.
228 *
229 * @param string $method The name of the method.
230 *
231 * @throws Prototype\MethodNotDefined if the method is not defined.
232 *
233 * @return callable
234 */
235 public function offsetGet($method)
236 {
237 $methods = $this->get_consolidated_methods();
238
239 if (!isset($methods[$method]))
240 {
241 throw new Prototype\MethodNotDefined([ $method, $this->class ]);
242 }
243
244 return $methods[$method];
245 }
246
247 /**
248 * Returns an iterator for the prototype methods.
249 *
250 * @see IteratorAggregate::getIterator()
251 */
252 public function getIterator()
253 {
254 $methods = $this->get_consolidated_methods();
255
256 return new \ArrayIterator($methods);
257 }
258 }
259
260 namespace ICanBoogie\Prototype;
261
262 /**
263 * This exception is thrown when one tries to access an undefined prototype method.
264 */
265 class MethodNotDefined extends \BadMethodCallException
266 {
267 public function __construct($message, $code=500, \Exception $previous=null)
268 {
269 if (is_array($message))
270 {
271 $message = sprintf('Method "%s" is not defined by the prototype of class "%s".', $message[0], $message[1]);
272 }
273
274 parent::__construct($message, $code, $previous);
275 }
276 }