b4c1da83bf6eaa544a9a788cf3d01aace1589a7c
[yaffs-website] / Dumper / OptimizedPhpArrayDumper.php
1 <?php
2
3 namespace Drupal\Component\DependencyInjection\Dumper;
4
5 use Drupal\Component\Utility\Crypt;
6 use Symfony\Component\DependencyInjection\ContainerInterface;
7 use Symfony\Component\DependencyInjection\Definition;
8 use Symfony\Component\DependencyInjection\Parameter;
9 use Symfony\Component\DependencyInjection\Reference;
10 use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
11 use Symfony\Component\DependencyInjection\Exception\RuntimeException;
12 use Symfony\Component\DependencyInjection\Dumper\Dumper;
13 use Symfony\Component\ExpressionLanguage\Expression;
14
15 /**
16  * OptimizedPhpArrayDumper dumps a service container as a serialized PHP array.
17  *
18  * The format of this dumper is very similar to the internal structure of the
19  * ContainerBuilder, but based on PHP arrays and \stdClass objects instead of
20  * rich value objects for performance reasons.
21  *
22  * By removing the abstraction and optimizing some cases like deep collections,
23  * fewer classes need to be loaded, fewer function calls need to be executed and
24  * fewer run time checks need to be made.
25  *
26  * In addition to that, this container dumper treats private services as
27  * strictly private with their own private services storage, whereas in the
28  * Symfony service container builder and PHP dumper, shared private services can
29  * still be retrieved via get() from the container.
30  *
31  * It is machine-optimized, for a human-readable version based on this one see
32  * \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper.
33  *
34  * @see \Drupal\Component\DependencyInjection\Container
35  */
36 class OptimizedPhpArrayDumper extends Dumper {
37
38   /**
39    * Whether to serialize service definitions or not.
40    *
41    * Service definitions are serialized by default to avoid having to
42    * unserialize the whole container on loading time, which improves early
43    * bootstrap performance for e.g. the page cache.
44    *
45    * @var bool
46    */
47   protected $serialize = TRUE;
48
49   /**
50    * {@inheritdoc}
51    */
52   public function dump(array $options = []) {
53     return serialize($this->getArray());
54   }
55
56   /**
57    * Gets the service container definition as a PHP array.
58    *
59    * @return array
60    *   A PHP array representation of the service container.
61    */
62   public function getArray() {
63     $definition = [];
64     $this->aliases = $this->getAliases();
65     $definition['aliases'] = $this->getAliases();
66     $definition['parameters'] = $this->getParameters();
67     $definition['services'] = $this->getServiceDefinitions();
68     $definition['frozen'] = $this->container->isCompiled();
69     $definition['machine_format'] = $this->supportsMachineFormat();
70     return $definition;
71   }
72
73   /**
74    * Gets the aliases as a PHP array.
75    *
76    * @return array
77    *   The aliases.
78    */
79   protected function getAliases() {
80     $alias_definitions = [];
81
82     $aliases = $this->container->getAliases();
83     foreach ($aliases as $alias => $id) {
84       $id = (string) $id;
85       while (isset($aliases[$id])) {
86         $id = (string) $aliases[$id];
87       }
88       $alias_definitions[$alias] = $id;
89     }
90
91     return $alias_definitions;
92   }
93
94   /**
95    * Gets parameters of the container as a PHP array.
96    *
97    * @return array
98    *   The escaped and prepared parameters of the container.
99    */
100   protected function getParameters() {
101     if (!$this->container->getParameterBag()->all()) {
102       return [];
103     }
104
105     $parameters = $this->container->getParameterBag()->all();
106     $is_compiled = $this->container->isCompiled();
107     return $this->prepareParameters($parameters, $is_compiled);
108   }
109
110   /**
111    * Gets services of the container as a PHP array.
112    *
113    * @return array
114    *   The service definitions.
115    */
116   protected function getServiceDefinitions() {
117     if (!$this->container->getDefinitions()) {
118       return [];
119     }
120
121     $services = [];
122     foreach ($this->container->getDefinitions() as $id => $definition) {
123       // Only store public service definitions, references to shared private
124       // services are handled in ::getReferenceCall().
125       if ($definition->isPublic()) {
126         $service_definition = $this->getServiceDefinition($definition);
127         $services[$id] = $this->serialize ? serialize($service_definition) : $service_definition;
128       }
129     }
130
131     return $services;
132   }
133
134   /**
135    * Prepares parameters for the PHP array dumping.
136    *
137    * @param array $parameters
138    *   An array of parameters.
139    * @param bool $escape
140    *   Whether keys with '%' should be escaped or not.
141    *
142    * @return array
143    *   An array of prepared parameters.
144    */
145   protected function prepareParameters(array $parameters, $escape = TRUE) {
146     $filtered = [];
147     foreach ($parameters as $key => $value) {
148       if (is_array($value)) {
149         $value = $this->prepareParameters($value, $escape);
150       }
151       elseif ($value instanceof Reference) {
152         $value = $this->dumpValue($value);
153       }
154
155       $filtered[$key] = $value;
156     }
157
158     return $escape ? $this->escape($filtered) : $filtered;
159   }
160
161   /**
162    * Escapes parameters.
163    *
164    * @param array $parameters
165    *   The parameters to escape for '%' characters.
166    *
167    * @return array
168    *   The escaped parameters.
169    */
170   protected function escape(array $parameters) {
171     $args = [];
172
173     foreach ($parameters as $key => $value) {
174       if (is_array($value)) {
175         $args[$key] = $this->escape($value);
176       }
177       elseif (is_string($value)) {
178         $args[$key] = str_replace('%', '%%', $value);
179       }
180       else {
181         $args[$key] = $value;
182       }
183     }
184
185     return $args;
186   }
187
188   /**
189    * Gets a service definition as PHP array.
190    *
191    * @param \Symfony\Component\DependencyInjection\Definition $definition
192    *   The definition to process.
193    *
194    * @return array
195    *   The service definition as PHP array.
196    *
197    * @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
198    *   Thrown when the definition is marked as decorated, or with an explicit
199    *   scope different from SCOPE_CONTAINER and SCOPE_PROTOTYPE.
200    */
201   protected function getServiceDefinition(Definition $definition) {
202     $service = [];
203     if ($definition->getClass()) {
204       $service['class'] = $definition->getClass();
205     }
206
207     if (!$definition->isPublic()) {
208       $service['public'] = FALSE;
209     }
210
211     if ($definition->getFile()) {
212       $service['file'] = $definition->getFile();
213     }
214
215     if ($definition->isSynthetic()) {
216       $service['synthetic'] = TRUE;
217     }
218
219     if ($definition->isLazy()) {
220       $service['lazy'] = TRUE;
221     }
222
223     if ($definition->getArguments()) {
224       $arguments = $definition->getArguments();
225       $service['arguments'] = $this->dumpCollection($arguments);
226       $service['arguments_count'] = count($arguments);
227     }
228     else {
229       $service['arguments_count'] = 0;
230     }
231
232     if ($definition->getProperties()) {
233       $service['properties'] = $this->dumpCollection($definition->getProperties());
234     }
235
236     if ($definition->getMethodCalls()) {
237       $service['calls'] = $this->dumpMethodCalls($definition->getMethodCalls());
238     }
239
240     // By default services are shared, so just provide the flag, when needed.
241     if ($definition->isShared() === FALSE) {
242       $service['shared'] = $definition->isShared();
243     }
244
245     if (($decorated = $definition->getDecoratedService()) !== NULL) {
246       throw new InvalidArgumentException("The 'decorated' definition is not supported by the Drupal 8 run-time container. The Container Builder should have resolved that during the DecoratorServicePass compiler pass.");
247     }
248
249     if ($callable = $definition->getFactory()) {
250       $service['factory'] = $this->dumpCallable($callable);
251     }
252
253     if ($callable = $definition->getConfigurator()) {
254       $service['configurator'] = $this->dumpCallable($callable);
255     }
256
257     return $service;
258   }
259
260   /**
261    * Dumps method calls to a PHP array.
262    *
263    * @param array $calls
264    *   An array of method calls.
265    *
266    * @return array
267    *   The PHP array representation of the method calls.
268    */
269   protected function dumpMethodCalls(array $calls) {
270     $code = [];
271
272     foreach ($calls as $key => $call) {
273       $method = $call[0];
274       $arguments = [];
275       if (!empty($call[1])) {
276         $arguments = $this->dumpCollection($call[1]);
277       }
278
279       $code[$key] = [$method, $arguments];
280     }
281
282     return $code;
283   }
284
285   /**
286    * Dumps a collection to a PHP array.
287    *
288    * @param mixed $collection
289    *   A collection to process.
290    * @param bool &$resolve
291    *   Used for passing the information to the caller whether the given
292    *   collection needed to be resolved or not. This is used for optimizing
293    *   deep arrays that don't need to be traversed.
294    *
295    * @return \stdClass|array
296    *   The collection in a suitable format.
297    */
298   protected function dumpCollection($collection, &$resolve = FALSE) {
299     $code = [];
300
301     foreach ($collection as $key => $value) {
302       if (is_array($value)) {
303         $resolve_collection = FALSE;
304         $code[$key] = $this->dumpCollection($value, $resolve_collection);
305
306         if ($resolve_collection) {
307           $resolve = TRUE;
308         }
309       }
310       else {
311         $code[$key] = $this->dumpValue($value);
312         if (is_object($code[$key])) {
313           $resolve = TRUE;
314         }
315       }
316     }
317
318     if (!$resolve) {
319       return $collection;
320     }
321
322     return (object) [
323       'type' => 'collection',
324       'value' => $code,
325       'resolve' => $resolve,
326     ];
327   }
328
329   /**
330    * Dumps callable to a PHP array.
331    *
332    * @param array|callable $callable
333    *   The callable to process.
334    *
335    * @return callable
336    *   The processed callable.
337    */
338   protected function dumpCallable($callable) {
339     if (is_array($callable)) {
340       $callable[0] = $this->dumpValue($callable[0]);
341       $callable = [$callable[0], $callable[1]];
342     }
343
344     return $callable;
345   }
346
347   /**
348    * Gets a private service definition in a suitable format.
349    *
350    * @param string $id
351    *   The ID of the service to get a private definition for.
352    * @param \Symfony\Component\DependencyInjection\Definition $definition
353    *   The definition to process.
354    * @param bool $shared
355    *   (optional) Whether the service will be shared with others.
356    *   By default this parameter is FALSE.
357    *
358    * @return \stdClass
359    *   A very lightweight private service value object.
360    */
361   protected function getPrivateServiceCall($id, Definition $definition, $shared = FALSE) {
362     $service_definition = $this->getServiceDefinition($definition);
363     if (!$id) {
364       $hash = Crypt::hashBase64(serialize($service_definition));
365       $id = 'private__' . $hash;
366     }
367     return (object) [
368       'type' => 'private_service',
369       'id' => $id,
370       'value' => $service_definition,
371       'shared' => $shared,
372     ];
373   }
374
375   /**
376    * Dumps the value to PHP array format.
377    *
378    * @param mixed $value
379    *   The value to dump.
380    *
381    * @return mixed
382    *   The dumped value in a suitable format.
383    *
384    * @throws RuntimeException
385    *   When trying to dump object or resource.
386    */
387   protected function dumpValue($value) {
388     if (is_array($value)) {
389       $code = [];
390       foreach ($value as $k => $v) {
391         $code[$k] = $this->dumpValue($v);
392       }
393
394       return $code;
395     }
396     elseif ($value instanceof Reference) {
397       return $this->getReferenceCall((string) $value, $value);
398     }
399     elseif ($value instanceof Definition) {
400       return $this->getPrivateServiceCall(NULL, $value);
401     }
402     elseif ($value instanceof Parameter) {
403       return $this->getParameterCall((string) $value);
404     }
405     elseif (is_string($value) && preg_match('/^\%(.*)\%$/', $value, $matches)) {
406       return $this->getParameterCall($matches[1]);
407     }
408     elseif ($value instanceof Expression) {
409       throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
410     }
411     elseif (is_object($value)) {
412       // Drupal specific: Instantiated objects have a _serviceId parameter.
413       if (isset($value->_serviceId)) {
414         return $this->getReferenceCall($value->_serviceId);
415       }
416       throw new RuntimeException('Unable to dump a service container if a parameter is an object without _serviceId.');
417     }
418     elseif (is_resource($value)) {
419       throw new RuntimeException('Unable to dump a service container if a parameter is a resource.');
420     }
421
422     return $value;
423   }
424
425   /**
426    * Gets a service reference for a reference in a suitable PHP array format.
427    *
428    * The main difference is that this function treats references to private
429    * services differently and returns a private service reference instead of
430    * a normal reference.
431    *
432    * @param string $id
433    *   The ID of the service to get a reference for.
434    * @param \Symfony\Component\DependencyInjection\Reference|null $reference
435    *   (optional) The reference object to process; needed to get the invalid
436    *   behavior value.
437    *
438    * @return string|\stdClass
439    *   A suitable representation of the service reference.
440    */
441   protected function getReferenceCall($id, Reference $reference = NULL) {
442     $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
443
444     if ($reference !== NULL) {
445       $invalid_behavior = $reference->getInvalidBehavior();
446     }
447
448     // Private shared service.
449     if (isset($this->aliases[$id])) {
450       $id = $this->aliases[$id];
451     }
452     $definition = $this->container->getDefinition($id);
453     if (!$definition->isPublic()) {
454       // The ContainerBuilder does not share a private service, but this means a
455       // new service is instantiated every time. Use a private shared service to
456       // circumvent the problem.
457       return $this->getPrivateServiceCall($id, $definition, TRUE);
458     }
459
460     return $this->getServiceCall($id, $invalid_behavior);
461   }
462
463   /**
464    * Gets a service reference for an ID in a suitable PHP array format.
465    *
466    * @param string $id
467    *   The ID of the service to get a reference for.
468    * @param int $invalid_behavior
469    *   (optional) The invalid behavior of the service.
470    *
471    * @return string|\stdClass
472    *   A suitable representation of the service reference.
473    */
474   protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
475     return (object) [
476       'type' => 'service',
477       'id' => $id,
478       'invalidBehavior' => $invalid_behavior,
479     ];
480   }
481
482   /**
483    * Gets a parameter reference in a suitable PHP array format.
484    *
485    * @param string $name
486    *   The name of the parameter to get a reference for.
487    *
488    * @return string|\stdClass
489    *   A suitable representation of the parameter reference.
490    */
491   protected function getParameterCall($name) {
492     return (object) [
493       'type' => 'parameter',
494       'name' => $name,
495     ];
496   }
497
498   /**
499    * Whether this supports the machine-optimized format or not.
500    *
501    * @return bool
502    *   TRUE if this supports machine-optimized format, FALSE otherwise.
503    */
504   protected function supportsMachineFormat() {
505     return TRUE;
506   }
507
508 }