JsonDescriptor.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor;
  11. use Symfony\Component\Console\Exception\LogicException;
  12. use Symfony\Component\Console\Exception\RuntimeException;
  13. use Symfony\Component\DependencyInjection\Alias;
  14. use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
  15. use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
  16. use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
  17. use Symfony\Component\DependencyInjection\ContainerBuilder;
  18. use Symfony\Component\DependencyInjection\Definition;
  19. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
  20. use Symfony\Component\DependencyInjection\Reference;
  21. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  22. use Symfony\Component\Routing\Route;
  23. use Symfony\Component\Routing\RouteCollection;
  24. /**
  25. * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  26. *
  27. * @internal
  28. */
  29. class JsonDescriptor extends Descriptor
  30. {
  31. protected function describeRouteCollection(RouteCollection $routes, array $options = [])
  32. {
  33. $data = [];
  34. foreach ($routes->all() as $name => $route) {
  35. $data[$name] = $this->getRouteData($route);
  36. }
  37. $this->writeData($data, $options);
  38. }
  39. protected function describeRoute(Route $route, array $options = [])
  40. {
  41. $this->writeData($this->getRouteData($route), $options);
  42. }
  43. protected function describeContainerParameters(ParameterBag $parameters, array $options = [])
  44. {
  45. $this->writeData($this->sortParameters($parameters), $options);
  46. }
  47. protected function describeContainerTags(ContainerBuilder $builder, array $options = [])
  48. {
  49. $showHidden = isset($options['show_hidden']) && $options['show_hidden'];
  50. $data = [];
  51. foreach ($this->findDefinitionsByTag($builder, $showHidden) as $tag => $definitions) {
  52. $data[$tag] = [];
  53. foreach ($definitions as $definition) {
  54. $data[$tag][] = $this->getContainerDefinitionData($definition, true);
  55. }
  56. }
  57. $this->writeData($data, $options);
  58. }
  59. protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null)
  60. {
  61. if (!isset($options['id'])) {
  62. throw new \InvalidArgumentException('An "id" option must be provided.');
  63. }
  64. if ($service instanceof Alias) {
  65. $this->describeContainerAlias($service, $options, $builder);
  66. } elseif ($service instanceof Definition) {
  67. $this->writeData($this->getContainerDefinitionData($service, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments']), $options);
  68. } else {
  69. $this->writeData(\get_class($service), $options);
  70. }
  71. }
  72. protected function describeContainerServices(ContainerBuilder $builder, array $options = [])
  73. {
  74. $serviceIds = isset($options['tag']) && $options['tag']
  75. ? $this->sortTaggedServicesByPriority($builder->findTaggedServiceIds($options['tag']))
  76. : $this->sortServiceIds($builder->getServiceIds());
  77. $showHidden = isset($options['show_hidden']) && $options['show_hidden'];
  78. $omitTags = isset($options['omit_tags']) && $options['omit_tags'];
  79. $showArguments = isset($options['show_arguments']) && $options['show_arguments'];
  80. $data = ['definitions' => [], 'aliases' => [], 'services' => []];
  81. if (isset($options['filter'])) {
  82. $serviceIds = array_filter($serviceIds, $options['filter']);
  83. }
  84. foreach ($serviceIds as $serviceId) {
  85. $service = $this->resolveServiceDefinition($builder, $serviceId);
  86. if ($showHidden xor '.' === ($serviceId[0] ?? null)) {
  87. continue;
  88. }
  89. if ($service instanceof Alias) {
  90. $data['aliases'][$serviceId] = $this->getContainerAliasData($service);
  91. } elseif ($service instanceof Definition) {
  92. $data['definitions'][$serviceId] = $this->getContainerDefinitionData($service, $omitTags, $showArguments);
  93. } else {
  94. $data['services'][$serviceId] = \get_class($service);
  95. }
  96. }
  97. $this->writeData($data, $options);
  98. }
  99. protected function describeContainerDefinition(Definition $definition, array $options = [])
  100. {
  101. $this->writeData($this->getContainerDefinitionData($definition, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments']), $options);
  102. }
  103. protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null)
  104. {
  105. if (!$builder) {
  106. $this->writeData($this->getContainerAliasData($alias), $options);
  107. return;
  108. }
  109. $this->writeData(
  110. [$this->getContainerAliasData($alias), $this->getContainerDefinitionData($builder->getDefinition((string) $alias), isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments'])],
  111. array_merge($options, ['id' => (string) $alias])
  112. );
  113. }
  114. protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = [])
  115. {
  116. $this->writeData($this->getEventDispatcherListenersData($eventDispatcher, \array_key_exists('event', $options) ? $options['event'] : null), $options);
  117. }
  118. protected function describeCallable($callable, array $options = [])
  119. {
  120. $this->writeData($this->getCallableData($callable), $options);
  121. }
  122. protected function describeContainerParameter($parameter, array $options = [])
  123. {
  124. $key = $options['parameter'] ?? '';
  125. $this->writeData([$key => $parameter], $options);
  126. }
  127. protected function describeContainerEnvVars(array $envs, array $options = [])
  128. {
  129. throw new LogicException('Using the JSON format to debug environment variables is not supported.');
  130. }
  131. protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void
  132. {
  133. $containerDeprecationFilePath = sprintf('%s/%sDeprecations.log', $builder->getParameter('kernel.build_dir'), $builder->getParameter('kernel.container_class'));
  134. if (!file_exists($containerDeprecationFilePath)) {
  135. throw new RuntimeException('The deprecation file does not exist, please try warming the cache first.');
  136. }
  137. $logs = unserialize(file_get_contents($containerDeprecationFilePath));
  138. $formattedLogs = [];
  139. $remainingCount = 0;
  140. foreach ($logs as $log) {
  141. $formattedLogs[] = [
  142. 'message' => $log['message'],
  143. 'file' => $log['file'],
  144. 'line' => $log['line'],
  145. 'count' => $log['count'],
  146. ];
  147. $remainingCount += $log['count'];
  148. }
  149. $this->writeData(['remainingCount' => $remainingCount, 'deprecations' => $formattedLogs], $options);
  150. }
  151. private function writeData(array $data, array $options)
  152. {
  153. $flags = $options['json_encoding'] ?? 0;
  154. $this->write(json_encode($data, $flags | \JSON_PRETTY_PRINT)."\n");
  155. }
  156. protected function getRouteData(Route $route): array
  157. {
  158. $data = [
  159. 'path' => $route->getPath(),
  160. 'pathRegex' => $route->compile()->getRegex(),
  161. 'host' => '' !== $route->getHost() ? $route->getHost() : 'ANY',
  162. 'hostRegex' => '' !== $route->getHost() ? $route->compile()->getHostRegex() : '',
  163. 'scheme' => $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY',
  164. 'method' => $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY',
  165. 'class' => \get_class($route),
  166. 'defaults' => $route->getDefaults(),
  167. 'requirements' => $route->getRequirements() ?: 'NO CUSTOM',
  168. 'options' => $route->getOptions(),
  169. ];
  170. if ('' !== $route->getCondition()) {
  171. $data['condition'] = $route->getCondition();
  172. }
  173. return $data;
  174. }
  175. private function getContainerDefinitionData(Definition $definition, bool $omitTags = false, bool $showArguments = false): array
  176. {
  177. $data = [
  178. 'class' => (string) $definition->getClass(),
  179. 'public' => $definition->isPublic() && !$definition->isPrivate(),
  180. 'synthetic' => $definition->isSynthetic(),
  181. 'lazy' => $definition->isLazy(),
  182. 'shared' => $definition->isShared(),
  183. 'abstract' => $definition->isAbstract(),
  184. 'autowire' => $definition->isAutowired(),
  185. 'autoconfigure' => $definition->isAutoconfigured(),
  186. ];
  187. if ('' !== $classDescription = $this->getClassDescription((string) $definition->getClass())) {
  188. $data['description'] = $classDescription;
  189. }
  190. if ($showArguments) {
  191. $data['arguments'] = $this->describeValue($definition->getArguments(), $omitTags, $showArguments);
  192. }
  193. $data['file'] = $definition->getFile();
  194. if ($factory = $definition->getFactory()) {
  195. if (\is_array($factory)) {
  196. if ($factory[0] instanceof Reference) {
  197. $data['factory_service'] = (string) $factory[0];
  198. } elseif ($factory[0] instanceof Definition) {
  199. throw new \InvalidArgumentException('Factory is not describable.');
  200. } else {
  201. $data['factory_class'] = $factory[0];
  202. }
  203. $data['factory_method'] = $factory[1];
  204. } else {
  205. $data['factory_function'] = $factory;
  206. }
  207. }
  208. $calls = $definition->getMethodCalls();
  209. if (\count($calls) > 0) {
  210. $data['calls'] = [];
  211. foreach ($calls as $callData) {
  212. $data['calls'][] = $callData[0];
  213. }
  214. }
  215. if (!$omitTags) {
  216. $data['tags'] = [];
  217. foreach ($this->sortTagsByPriority($definition->getTags()) as $tagName => $tagData) {
  218. foreach ($tagData as $parameters) {
  219. $data['tags'][] = ['name' => $tagName, 'parameters' => $parameters];
  220. }
  221. }
  222. }
  223. return $data;
  224. }
  225. private function getContainerAliasData(Alias $alias): array
  226. {
  227. return [
  228. 'service' => (string) $alias,
  229. 'public' => $alias->isPublic() && !$alias->isPrivate(),
  230. ];
  231. }
  232. private function getEventDispatcherListenersData(EventDispatcherInterface $eventDispatcher, string $event = null): array
  233. {
  234. $data = [];
  235. $registeredListeners = $eventDispatcher->getListeners($event);
  236. if (null !== $event) {
  237. foreach ($registeredListeners as $listener) {
  238. $l = $this->getCallableData($listener);
  239. $l['priority'] = $eventDispatcher->getListenerPriority($event, $listener);
  240. $data[] = $l;
  241. }
  242. } else {
  243. ksort($registeredListeners);
  244. foreach ($registeredListeners as $eventListened => $eventListeners) {
  245. foreach ($eventListeners as $eventListener) {
  246. $l = $this->getCallableData($eventListener);
  247. $l['priority'] = $eventDispatcher->getListenerPriority($eventListened, $eventListener);
  248. $data[$eventListened][] = $l;
  249. }
  250. }
  251. }
  252. return $data;
  253. }
  254. private function getCallableData($callable): array
  255. {
  256. $data = [];
  257. if (\is_array($callable)) {
  258. $data['type'] = 'function';
  259. if (\is_object($callable[0])) {
  260. $data['name'] = $callable[1];
  261. $data['class'] = \get_class($callable[0]);
  262. } else {
  263. if (0 !== strpos($callable[1], 'parent::')) {
  264. $data['name'] = $callable[1];
  265. $data['class'] = $callable[0];
  266. $data['static'] = true;
  267. } else {
  268. $data['name'] = substr($callable[1], 8);
  269. $data['class'] = $callable[0];
  270. $data['static'] = true;
  271. $data['parent'] = true;
  272. }
  273. }
  274. return $data;
  275. }
  276. if (\is_string($callable)) {
  277. $data['type'] = 'function';
  278. if (false === strpos($callable, '::')) {
  279. $data['name'] = $callable;
  280. } else {
  281. $callableParts = explode('::', $callable);
  282. $data['name'] = $callableParts[1];
  283. $data['class'] = $callableParts[0];
  284. $data['static'] = true;
  285. }
  286. return $data;
  287. }
  288. if ($callable instanceof \Closure) {
  289. $data['type'] = 'closure';
  290. $r = new \ReflectionFunction($callable);
  291. if (false !== strpos($r->name, '{closure}')) {
  292. return $data;
  293. }
  294. $data['name'] = $r->name;
  295. if ($class = $r->getClosureScopeClass()) {
  296. $data['class'] = $class->name;
  297. if (!$r->getClosureThis()) {
  298. $data['static'] = true;
  299. }
  300. }
  301. return $data;
  302. }
  303. if (method_exists($callable, '__invoke')) {
  304. $data['type'] = 'object';
  305. $data['name'] = \get_class($callable);
  306. return $data;
  307. }
  308. throw new \InvalidArgumentException('Callable is not describable.');
  309. }
  310. private function describeValue($value, bool $omitTags, bool $showArguments)
  311. {
  312. if (\is_array($value)) {
  313. $data = [];
  314. foreach ($value as $k => $v) {
  315. $data[$k] = $this->describeValue($v, $omitTags, $showArguments);
  316. }
  317. return $data;
  318. }
  319. if ($value instanceof ServiceClosureArgument) {
  320. $value = $value->getValues()[0];
  321. }
  322. if ($value instanceof Reference) {
  323. return [
  324. 'type' => 'service',
  325. 'id' => (string) $value,
  326. ];
  327. }
  328. if ($value instanceof AbstractArgument) {
  329. return ['type' => 'abstract', 'text' => $value->getText()];
  330. }
  331. if ($value instanceof ArgumentInterface) {
  332. return $this->describeValue($value->getValues(), $omitTags, $showArguments);
  333. }
  334. if ($value instanceof Definition) {
  335. return $this->getContainerDefinitionData($value, $omitTags, $showArguments);
  336. }
  337. return $value;
  338. }
  339. }