]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
1 | <?php |
2 | ||
3 | /* | |
4 | * This file is part of the Symfony package. | |
5 | * | |
6 | * (c) Fabien Potencier <fabien@symfony.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 Symfony\Component\OptionsResolver; | |
13 | ||
14 | use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; | |
15 | ||
16 | /** | |
17 | * Container for resolving inter-dependent options. | |
18 | * | |
19 | * @author Bernhard Schussek <bschussek@gmail.com> | |
20 | */ | |
21 | class Options implements \ArrayAccess, \Iterator, \Countable | |
22 | { | |
23 | /** | |
24 | * A list of option values. | |
25 | * @var array | |
26 | */ | |
27 | private $options = array(); | |
28 | ||
29 | /** | |
30 | * A list of normalizer closures. | |
31 | * @var array | |
32 | */ | |
33 | private $normalizers = array(); | |
34 | ||
35 | /** | |
36 | * A list of closures for evaluating lazy options. | |
37 | * @var array | |
38 | */ | |
39 | private $lazy = array(); | |
40 | ||
41 | /** | |
42 | * A list containing the currently locked options. | |
43 | * @var array | |
44 | */ | |
45 | private $lock = array(); | |
46 | ||
47 | /** | |
48 | * Whether at least one option has already been read. | |
49 | * | |
50 | * Once read, the options cannot be changed anymore. This is | |
51 | * necessary in order to avoid inconsistencies during the resolving | |
52 | * process. If any option is changed after being read, all evaluated | |
53 | * lazy options that depend on this option would become invalid. | |
54 | * | |
55 | * @var Boolean | |
56 | */ | |
57 | private $reading = false; | |
58 | ||
59 | /** | |
60 | * Sets the value of a given option. | |
61 | * | |
62 | * You can set lazy options by passing a closure with the following | |
63 | * signature: | |
64 | * | |
65 | * <code> | |
66 | * function (Options $options) | |
67 | * </code> | |
68 | * | |
69 | * This closure will be evaluated once the option is read using | |
70 | * {@link get()}. The closure has access to the resolved values of | |
71 | * other options through the passed {@link Options} instance. | |
72 | * | |
73 | * @param string $option The name of the option. | |
74 | * @param mixed $value The value of the option. | |
75 | * | |
76 | * @throws OptionDefinitionException If options have already been read. | |
77 | * Once options are read, the container | |
78 | * becomes immutable. | |
79 | */ | |
80 | public function set($option, $value) | |
81 | { | |
82 | // Setting is not possible once an option is read, because then lazy | |
83 | // options could manipulate the state of the object, leading to | |
84 | // inconsistent results. | |
85 | if ($this->reading) { | |
86 | throw new OptionDefinitionException('Options cannot be set anymore once options have been read.'); | |
87 | } | |
88 | ||
89 | // Setting is equivalent to overloading while discarding the previous | |
90 | // option value | |
91 | unset($this->options[$option]); | |
92 | unset($this->lazy[$option]); | |
93 | ||
94 | $this->overload($option, $value); | |
95 | } | |
96 | ||
97 | /** | |
98 | * Sets the normalizer for a given option. | |
99 | * | |
100 | * Normalizers should be closures with the following signature: | |
101 | * | |
102 | * <code> | |
103 | * function (Options $options, $value) | |
104 | * </code> | |
105 | * | |
106 | * This closure will be evaluated once the option is read using | |
107 | * {@link get()}. The closure has access to the resolved values of | |
108 | * other options through the passed {@link Options} instance. | |
109 | * | |
110 | * @param string $option The name of the option. | |
111 | * @param \Closure $normalizer The normalizer. | |
112 | * | |
113 | * @throws OptionDefinitionException If options have already been read. | |
114 | * Once options are read, the container | |
115 | * becomes immutable. | |
116 | */ | |
117 | public function setNormalizer($option, \Closure $normalizer) | |
118 | { | |
119 | if ($this->reading) { | |
120 | throw new OptionDefinitionException('Normalizers cannot be added anymore once options have been read.'); | |
121 | } | |
122 | ||
123 | $this->normalizers[$option] = $normalizer; | |
124 | } | |
125 | ||
126 | /** | |
127 | * Replaces the contents of the container with the given options. | |
128 | * | |
129 | * This method is a shortcut for {@link clear()} with subsequent | |
130 | * calls to {@link set()}. | |
131 | * | |
132 | * @param array $options The options to set. | |
133 | * | |
134 | * @throws OptionDefinitionException If options have already been read. | |
135 | * Once options are read, the container | |
136 | * becomes immutable. | |
137 | */ | |
138 | public function replace(array $options) | |
139 | { | |
140 | if ($this->reading) { | |
141 | throw new OptionDefinitionException('Options cannot be replaced anymore once options have been read.'); | |
142 | } | |
143 | ||
144 | $this->options = array(); | |
145 | $this->lazy = array(); | |
146 | $this->normalizers = array(); | |
147 | ||
148 | foreach ($options as $option => $value) { | |
149 | $this->overload($option, $value); | |
150 | } | |
151 | } | |
152 | ||
153 | /** | |
154 | * Overloads the value of a given option. | |
155 | * | |
156 | * Contrary to {@link set()}, this method keeps the previous default | |
157 | * value of the option so that you can access it if you pass a closure. | |
158 | * Passed closures should have the following signature: | |
159 | * | |
160 | * <code> | |
161 | * function (Options $options, $value) | |
162 | * </code> | |
163 | * | |
164 | * The second parameter passed to the closure is the current default | |
165 | * value of the option. | |
166 | * | |
167 | * @param string $option The option name. | |
168 | * @param mixed $value The option value. | |
169 | * | |
170 | * @throws OptionDefinitionException If options have already been read. | |
171 | * Once options are read, the container | |
172 | * becomes immutable. | |
173 | */ | |
174 | public function overload($option, $value) | |
175 | { | |
176 | if ($this->reading) { | |
177 | throw new OptionDefinitionException('Options cannot be overloaded anymore once options have been read.'); | |
178 | } | |
179 | ||
180 | // If an option is a closure that should be evaluated lazily, store it | |
181 | // in the "lazy" property. | |
182 | if ($value instanceof \Closure) { | |
183 | $reflClosure = new \ReflectionFunction($value); | |
184 | $params = $reflClosure->getParameters(); | |
185 | ||
186 | if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && __CLASS__ === $class->name) { | |
187 | // Initialize the option if no previous value exists | |
188 | if (!isset($this->options[$option])) { | |
189 | $this->options[$option] = null; | |
190 | } | |
191 | ||
192 | // Ignore previous lazy options if the closure has no second parameter | |
193 | if (!isset($this->lazy[$option]) || !isset($params[1])) { | |
194 | $this->lazy[$option] = array(); | |
195 | } | |
196 | ||
197 | // Store closure for later evaluation | |
198 | $this->lazy[$option][] = $value; | |
199 | ||
200 | return; | |
201 | } | |
202 | } | |
203 | ||
204 | // Remove lazy options by default | |
205 | unset($this->lazy[$option]); | |
206 | ||
207 | $this->options[$option] = $value; | |
208 | } | |
209 | ||
210 | /** | |
211 | * Returns the value of the given option. | |
212 | * | |
213 | * If the option was a lazy option, it is evaluated now. | |
214 | * | |
215 | * @param string $option The option name. | |
216 | * | |
217 | * @return mixed The option value. | |
218 | * | |
219 | * @throws \OutOfBoundsException If the option does not exist. | |
220 | * @throws OptionDefinitionException If a cyclic dependency is detected | |
221 | * between two lazy options. | |
222 | */ | |
223 | public function get($option) | |
224 | { | |
225 | $this->reading = true; | |
226 | ||
227 | if (!array_key_exists($option, $this->options)) { | |
228 | throw new \OutOfBoundsException(sprintf('The option "%s" does not exist.', $option)); | |
229 | } | |
230 | ||
231 | if (isset($this->lazy[$option])) { | |
232 | $this->resolve($option); | |
233 | } | |
234 | ||
235 | if (isset($this->normalizers[$option])) { | |
236 | $this->normalize($option); | |
237 | } | |
238 | ||
239 | return $this->options[$option]; | |
240 | } | |
241 | ||
242 | /** | |
243 | * Returns whether the given option exists. | |
244 | * | |
245 | * @param string $option The option name. | |
246 | * | |
247 | * @return Boolean Whether the option exists. | |
248 | */ | |
249 | public function has($option) | |
250 | { | |
251 | return array_key_exists($option, $this->options); | |
252 | } | |
253 | ||
254 | /** | |
255 | * Removes the option with the given name. | |
256 | * | |
257 | * @param string $option The option name. | |
258 | * | |
259 | * @throws OptionDefinitionException If options have already been read. | |
260 | * Once options are read, the container | |
261 | * becomes immutable. | |
262 | */ | |
263 | public function remove($option) | |
264 | { | |
265 | if ($this->reading) { | |
266 | throw new OptionDefinitionException('Options cannot be removed anymore once options have been read.'); | |
267 | } | |
268 | ||
269 | unset($this->options[$option]); | |
270 | unset($this->lazy[$option]); | |
271 | unset($this->normalizers[$option]); | |
272 | } | |
273 | ||
274 | /** | |
275 | * Removes all options. | |
276 | * | |
277 | * @throws OptionDefinitionException If options have already been read. | |
278 | * Once options are read, the container | |
279 | * becomes immutable. | |
280 | */ | |
281 | public function clear() | |
282 | { | |
283 | if ($this->reading) { | |
284 | throw new OptionDefinitionException('Options cannot be cleared anymore once options have been read.'); | |
285 | } | |
286 | ||
287 | $this->options = array(); | |
288 | $this->lazy = array(); | |
289 | $this->normalizers = array(); | |
290 | } | |
291 | ||
292 | /** | |
293 | * Returns the values of all options. | |
294 | * | |
295 | * Lazy options are evaluated at this point. | |
296 | * | |
297 | * @return array The option values. | |
298 | */ | |
299 | public function all() | |
300 | { | |
301 | $this->reading = true; | |
302 | ||
303 | // Performance-wise this is slightly better than | |
304 | // while (null !== $option = key($this->lazy)) | |
305 | foreach ($this->lazy as $option => $closures) { | |
306 | // Double check, in case the option has already been resolved | |
307 | // by cascade in the previous cycles | |
308 | if (isset($this->lazy[$option])) { | |
309 | $this->resolve($option); | |
310 | } | |
311 | } | |
312 | ||
313 | foreach ($this->normalizers as $option => $normalizer) { | |
314 | if (isset($this->normalizers[$option])) { | |
315 | $this->normalize($option); | |
316 | } | |
317 | } | |
318 | ||
319 | return $this->options; | |
320 | } | |
321 | ||
322 | /** | |
323 | * Equivalent to {@link has()}. | |
324 | * | |
325 | * @param string $option The option name. | |
326 | * | |
327 | * @return Boolean Whether the option exists. | |
328 | * | |
329 | * @see \ArrayAccess::offsetExists() | |
330 | */ | |
331 | public function offsetExists($option) | |
332 | { | |
333 | return $this->has($option); | |
334 | } | |
335 | ||
336 | /** | |
337 | * Equivalent to {@link get()}. | |
338 | * | |
339 | * @param string $option The option name. | |
340 | * | |
341 | * @return mixed The option value. | |
342 | * | |
343 | * @throws \OutOfBoundsException If the option does not exist. | |
344 | * @throws OptionDefinitionException If a cyclic dependency is detected | |
345 | * between two lazy options. | |
346 | * | |
347 | * @see \ArrayAccess::offsetGet() | |
348 | */ | |
349 | public function offsetGet($option) | |
350 | { | |
351 | return $this->get($option); | |
352 | } | |
353 | ||
354 | /** | |
355 | * Equivalent to {@link set()}. | |
356 | * | |
357 | * @param string $option The name of the option. | |
358 | * @param mixed $value The value of the option. May be a closure with a | |
359 | * signature as defined in DefaultOptions::add(). | |
360 | * | |
361 | * @throws OptionDefinitionException If options have already been read. | |
362 | * Once options are read, the container | |
363 | * becomes immutable. | |
364 | * | |
365 | * @see \ArrayAccess::offsetSet() | |
366 | */ | |
367 | public function offsetSet($option, $value) | |
368 | { | |
369 | $this->set($option, $value); | |
370 | } | |
371 | ||
372 | /** | |
373 | * Equivalent to {@link remove()}. | |
374 | * | |
375 | * @param string $option The option name. | |
376 | * | |
377 | * @throws OptionDefinitionException If options have already been read. | |
378 | * Once options are read, the container | |
379 | * becomes immutable. | |
380 | * | |
381 | * @see \ArrayAccess::offsetUnset() | |
382 | */ | |
383 | public function offsetUnset($option) | |
384 | { | |
385 | $this->remove($option); | |
386 | } | |
387 | ||
388 | /** | |
389 | * {@inheritdoc} | |
390 | */ | |
391 | public function current() | |
392 | { | |
393 | return $this->get($this->key()); | |
394 | } | |
395 | ||
396 | /** | |
397 | * {@inheritdoc} | |
398 | */ | |
399 | public function next() | |
400 | { | |
401 | next($this->options); | |
402 | } | |
403 | ||
404 | /** | |
405 | * {@inheritdoc} | |
406 | */ | |
407 | public function key() | |
408 | { | |
409 | return key($this->options); | |
410 | } | |
411 | ||
412 | /** | |
413 | * {@inheritdoc} | |
414 | */ | |
415 | public function valid() | |
416 | { | |
417 | return null !== $this->key(); | |
418 | } | |
419 | ||
420 | /** | |
421 | * {@inheritdoc} | |
422 | */ | |
423 | public function rewind() | |
424 | { | |
425 | reset($this->options); | |
426 | } | |
427 | ||
428 | /** | |
429 | * {@inheritdoc} | |
430 | */ | |
431 | public function count() | |
432 | { | |
433 | return count($this->options); | |
434 | } | |
435 | ||
436 | /** | |
437 | * Evaluates the given lazy option. | |
438 | * | |
439 | * The evaluated value is written into the options array. The closure for | |
440 | * evaluating the option is discarded afterwards. | |
441 | * | |
442 | * @param string $option The option to evaluate. | |
443 | * | |
444 | * @throws OptionDefinitionException If the option has a cyclic dependency | |
445 | * on another option. | |
446 | */ | |
447 | private function resolve($option) | |
448 | { | |
449 | // The code duplication with normalize() exists for performance | |
450 | // reasons, in order to save a method call. | |
451 | // Remember that this method is potentially called a couple of thousand | |
452 | // times and needs to be as efficient as possible. | |
453 | if (isset($this->lock[$option])) { | |
454 | $conflicts = array(); | |
455 | ||
456 | foreach ($this->lock as $option => $locked) { | |
457 | if ($locked) { | |
458 | $conflicts[] = $option; | |
459 | } | |
460 | } | |
461 | ||
462 | throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', $conflicts))); | |
463 | } | |
464 | ||
465 | $this->lock[$option] = true; | |
466 | foreach ($this->lazy[$option] as $closure) { | |
467 | $this->options[$option] = $closure($this, $this->options[$option]); | |
468 | } | |
469 | unset($this->lock[$option]); | |
470 | ||
471 | // The option now isn't lazy anymore | |
472 | unset($this->lazy[$option]); | |
473 | } | |
474 | ||
475 | /** | |
476 | * Normalizes the given option. | |
477 | * | |
478 | * The evaluated value is written into the options array. | |
479 | * | |
480 | * @param string $option The option to normalizer. | |
481 | * | |
482 | * @throws OptionDefinitionException If the option has a cyclic dependency | |
483 | * on another option. | |
484 | */ | |
485 | private function normalize($option) | |
486 | { | |
487 | // The code duplication with resolve() exists for performance | |
488 | // reasons, in order to save a method call. | |
489 | // Remember that this method is potentially called a couple of thousand | |
490 | // times and needs to be as efficient as possible. | |
491 | if (isset($this->lock[$option])) { | |
492 | $conflicts = array(); | |
493 | ||
494 | foreach ($this->lock as $option => $locked) { | |
495 | if ($locked) { | |
496 | $conflicts[] = $option; | |
497 | } | |
498 | } | |
499 | ||
500 | throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', $conflicts))); | |
501 | } | |
502 | ||
503 | /** @var \Closure $normalizer */ | |
504 | $normalizer = $this->normalizers[$option]; | |
505 | ||
506 | $this->lock[$option] = true; | |
507 | $this->options[$option] = $normalizer($this, array_key_exists($option, $this->options) ? $this->options[$option] : null); | |
508 | unset($this->lock[$option]); | |
509 | ||
510 | // The option is now normalized | |
511 | unset($this->normalizers[$option]); | |
512 | } | |
513 | } |