MarkdownDescriptor.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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\ContainerBuilder;
  15. use Symfony\Component\DependencyInjection\Definition;
  16. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
  17. use Symfony\Component\DependencyInjection\Reference;
  18. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  19. use Symfony\Component\Routing\Route;
  20. use Symfony\Component\Routing\RouteCollection;
  21. /**
  22. * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  23. *
  24. * @internal
  25. */
  26. class MarkdownDescriptor extends Descriptor
  27. {
  28. protected function describeRouteCollection(RouteCollection $routes, array $options = [])
  29. {
  30. $first = true;
  31. foreach ($routes->all() as $name => $route) {
  32. if ($first) {
  33. $first = false;
  34. } else {
  35. $this->write("\n\n");
  36. }
  37. $this->describeRoute($route, ['name' => $name]);
  38. }
  39. $this->write("\n");
  40. }
  41. protected function describeRoute(Route $route, array $options = [])
  42. {
  43. $output = '- Path: '.$route->getPath()
  44. ."\n".'- Path Regex: '.$route->compile()->getRegex()
  45. ."\n".'- Host: '.('' !== $route->getHost() ? $route->getHost() : 'ANY')
  46. ."\n".'- Host Regex: '.('' !== $route->getHost() ? $route->compile()->getHostRegex() : '')
  47. ."\n".'- Scheme: '.($route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY')
  48. ."\n".'- Method: '.($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY')
  49. ."\n".'- Class: '.\get_class($route)
  50. ."\n".'- Defaults: '.$this->formatRouterConfig($route->getDefaults())
  51. ."\n".'- Requirements: '.($route->getRequirements() ? $this->formatRouterConfig($route->getRequirements()) : 'NO CUSTOM')
  52. ."\n".'- Options: '.$this->formatRouterConfig($route->getOptions());
  53. if ('' !== $route->getCondition()) {
  54. $output .= "\n".'- Condition: '.$route->getCondition();
  55. }
  56. $this->write(isset($options['name'])
  57. ? $options['name']."\n".str_repeat('-', \strlen($options['name']))."\n\n".$output
  58. : $output);
  59. $this->write("\n");
  60. }
  61. protected function describeContainerParameters(ParameterBag $parameters, array $options = [])
  62. {
  63. $this->write("Container parameters\n====================\n");
  64. foreach ($this->sortParameters($parameters) as $key => $value) {
  65. $this->write(sprintf("\n- `%s`: `%s`", $key, $this->formatParameter($value)));
  66. }
  67. }
  68. protected function describeContainerTags(ContainerBuilder $builder, array $options = [])
  69. {
  70. $showHidden = isset($options['show_hidden']) && $options['show_hidden'];
  71. $this->write("Container tags\n==============");
  72. foreach ($this->findDefinitionsByTag($builder, $showHidden) as $tag => $definitions) {
  73. $this->write("\n\n".$tag."\n".str_repeat('-', \strlen($tag)));
  74. foreach ($definitions as $serviceId => $definition) {
  75. $this->write("\n\n");
  76. $this->describeContainerDefinition($definition, ['omit_tags' => true, 'id' => $serviceId]);
  77. }
  78. }
  79. }
  80. protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null)
  81. {
  82. if (!isset($options['id'])) {
  83. throw new \InvalidArgumentException('An "id" option must be provided.');
  84. }
  85. $childOptions = array_merge($options, ['id' => $options['id'], 'as_array' => true]);
  86. if ($service instanceof Alias) {
  87. $this->describeContainerAlias($service, $childOptions, $builder);
  88. } elseif ($service instanceof Definition) {
  89. $this->describeContainerDefinition($service, $childOptions);
  90. } else {
  91. $this->write(sprintf('**`%s`:** `%s`', $options['id'], \get_class($service)));
  92. }
  93. }
  94. protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void
  95. {
  96. $containerDeprecationFilePath = sprintf('%s/%sDeprecations.log', $builder->getParameter('kernel.build_dir'), $builder->getParameter('kernel.container_class'));
  97. if (!file_exists($containerDeprecationFilePath)) {
  98. throw new RuntimeException('The deprecation file does not exist, please try warming the cache first.');
  99. }
  100. $logs = unserialize(file_get_contents($containerDeprecationFilePath));
  101. if (0 === \count($logs)) {
  102. $this->write("## There are no deprecations in the logs!\n");
  103. return;
  104. }
  105. $formattedLogs = [];
  106. $remainingCount = 0;
  107. foreach ($logs as $log) {
  108. $formattedLogs[] = sprintf("- %sx: \"%s\" in %s:%s\n", $log['count'], $log['message'], $log['file'], $log['line']);
  109. $remainingCount += $log['count'];
  110. }
  111. $this->write(sprintf("## Remaining deprecations (%s)\n\n", $remainingCount));
  112. foreach ($formattedLogs as $formattedLog) {
  113. $this->write($formattedLog);
  114. }
  115. }
  116. protected function describeContainerServices(ContainerBuilder $builder, array $options = [])
  117. {
  118. $showHidden = isset($options['show_hidden']) && $options['show_hidden'];
  119. $title = $showHidden ? 'Hidden services' : 'Services';
  120. if (isset($options['tag'])) {
  121. $title .= ' with tag `'.$options['tag'].'`';
  122. }
  123. $this->write($title."\n".str_repeat('=', \strlen($title)));
  124. $serviceIds = isset($options['tag']) && $options['tag']
  125. ? $this->sortTaggedServicesByPriority($builder->findTaggedServiceIds($options['tag']))
  126. : $this->sortServiceIds($builder->getServiceIds());
  127. $showArguments = isset($options['show_arguments']) && $options['show_arguments'];
  128. $services = ['definitions' => [], 'aliases' => [], 'services' => []];
  129. if (isset($options['filter'])) {
  130. $serviceIds = array_filter($serviceIds, $options['filter']);
  131. }
  132. foreach ($serviceIds as $serviceId) {
  133. $service = $this->resolveServiceDefinition($builder, $serviceId);
  134. if ($showHidden xor '.' === ($serviceId[0] ?? null)) {
  135. continue;
  136. }
  137. if ($service instanceof Alias) {
  138. $services['aliases'][$serviceId] = $service;
  139. } elseif ($service instanceof Definition) {
  140. $services['definitions'][$serviceId] = $service;
  141. } else {
  142. $services['services'][$serviceId] = $service;
  143. }
  144. }
  145. if (!empty($services['definitions'])) {
  146. $this->write("\n\nDefinitions\n-----------\n");
  147. foreach ($services['definitions'] as $id => $service) {
  148. $this->write("\n");
  149. $this->describeContainerDefinition($service, ['id' => $id, 'show_arguments' => $showArguments]);
  150. }
  151. }
  152. if (!empty($services['aliases'])) {
  153. $this->write("\n\nAliases\n-------\n");
  154. foreach ($services['aliases'] as $id => $service) {
  155. $this->write("\n");
  156. $this->describeContainerAlias($service, ['id' => $id]);
  157. }
  158. }
  159. if (!empty($services['services'])) {
  160. $this->write("\n\nServices\n--------\n");
  161. foreach ($services['services'] as $id => $service) {
  162. $this->write("\n");
  163. $this->write(sprintf('- `%s`: `%s`', $id, \get_class($service)));
  164. }
  165. }
  166. }
  167. protected function describeContainerDefinition(Definition $definition, array $options = [])
  168. {
  169. $output = '';
  170. if ('' !== $classDescription = $this->getClassDescription((string) $definition->getClass())) {
  171. $output .= '- Description: `'.$classDescription.'`'."\n";
  172. }
  173. $output .= '- Class: `'.$definition->getClass().'`'
  174. ."\n".'- Public: '.($definition->isPublic() && !$definition->isPrivate() ? 'yes' : 'no')
  175. ."\n".'- Synthetic: '.($definition->isSynthetic() ? 'yes' : 'no')
  176. ."\n".'- Lazy: '.($definition->isLazy() ? 'yes' : 'no')
  177. ."\n".'- Shared: '.($definition->isShared() ? 'yes' : 'no')
  178. ."\n".'- Abstract: '.($definition->isAbstract() ? 'yes' : 'no')
  179. ."\n".'- Autowired: '.($definition->isAutowired() ? 'yes' : 'no')
  180. ."\n".'- Autoconfigured: '.($definition->isAutoconfigured() ? 'yes' : 'no')
  181. ;
  182. if (isset($options['show_arguments']) && $options['show_arguments']) {
  183. $output .= "\n".'- Arguments: '.($definition->getArguments() ? 'yes' : 'no');
  184. }
  185. if ($definition->getFile()) {
  186. $output .= "\n".'- File: `'.$definition->getFile().'`';
  187. }
  188. if ($factory = $definition->getFactory()) {
  189. if (\is_array($factory)) {
  190. if ($factory[0] instanceof Reference) {
  191. $output .= "\n".'- Factory Service: `'.$factory[0].'`';
  192. } elseif ($factory[0] instanceof Definition) {
  193. throw new \InvalidArgumentException('Factory is not describable.');
  194. } else {
  195. $output .= "\n".'- Factory Class: `'.$factory[0].'`';
  196. }
  197. $output .= "\n".'- Factory Method: `'.$factory[1].'`';
  198. } else {
  199. $output .= "\n".'- Factory Function: `'.$factory.'`';
  200. }
  201. }
  202. $calls = $definition->getMethodCalls();
  203. foreach ($calls as $callData) {
  204. $output .= "\n".'- Call: `'.$callData[0].'`';
  205. }
  206. if (!(isset($options['omit_tags']) && $options['omit_tags'])) {
  207. foreach ($this->sortTagsByPriority($definition->getTags()) as $tagName => $tagData) {
  208. foreach ($tagData as $parameters) {
  209. $output .= "\n".'- Tag: `'.$tagName.'`';
  210. foreach ($parameters as $name => $value) {
  211. $output .= "\n".' - '.ucfirst($name).': '.$value;
  212. }
  213. }
  214. }
  215. }
  216. $this->write(isset($options['id']) ? sprintf("### %s\n\n%s\n", $options['id'], $output) : $output);
  217. }
  218. protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null)
  219. {
  220. $output = '- Service: `'.$alias.'`'
  221. ."\n".'- Public: '.($alias->isPublic() && !$alias->isPrivate() ? 'yes' : 'no');
  222. if (!isset($options['id'])) {
  223. $this->write($output);
  224. return;
  225. }
  226. $this->write(sprintf("### %s\n\n%s\n", $options['id'], $output));
  227. if (!$builder) {
  228. return;
  229. }
  230. $this->write("\n");
  231. $this->describeContainerDefinition($builder->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias]));
  232. }
  233. protected function describeContainerParameter($parameter, array $options = [])
  234. {
  235. $this->write(isset($options['parameter']) ? sprintf("%s\n%s\n\n%s", $options['parameter'], str_repeat('=', \strlen($options['parameter'])), $this->formatParameter($parameter)) : $parameter);
  236. }
  237. protected function describeContainerEnvVars(array $envs, array $options = [])
  238. {
  239. throw new LogicException('Using the markdown format to debug environment variables is not supported.');
  240. }
  241. protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = [])
  242. {
  243. $event = \array_key_exists('event', $options) ? $options['event'] : null;
  244. $title = 'Registered listeners';
  245. if (null !== $event) {
  246. $title .= sprintf(' for event `%s` ordered by descending priority', $event);
  247. }
  248. $this->write(sprintf('# %s', $title)."\n");
  249. $registeredListeners = $eventDispatcher->getListeners($event);
  250. if (null !== $event) {
  251. foreach ($registeredListeners as $order => $listener) {
  252. $this->write("\n".sprintf('## Listener %d', $order + 1)."\n");
  253. $this->describeCallable($listener);
  254. $this->write(sprintf('- Priority: `%d`', $eventDispatcher->getListenerPriority($event, $listener))."\n");
  255. }
  256. } else {
  257. ksort($registeredListeners);
  258. foreach ($registeredListeners as $eventListened => $eventListeners) {
  259. $this->write("\n".sprintf('## %s', $eventListened)."\n");
  260. foreach ($eventListeners as $order => $eventListener) {
  261. $this->write("\n".sprintf('### Listener %d', $order + 1)."\n");
  262. $this->describeCallable($eventListener);
  263. $this->write(sprintf('- Priority: `%d`', $eventDispatcher->getListenerPriority($eventListened, $eventListener))."\n");
  264. }
  265. }
  266. }
  267. }
  268. protected function describeCallable($callable, array $options = [])
  269. {
  270. $string = '';
  271. if (\is_array($callable)) {
  272. $string .= "\n- Type: `function`";
  273. if (\is_object($callable[0])) {
  274. $string .= "\n".sprintf('- Name: `%s`', $callable[1]);
  275. $string .= "\n".sprintf('- Class: `%s`', \get_class($callable[0]));
  276. } else {
  277. if (0 !== strpos($callable[1], 'parent::')) {
  278. $string .= "\n".sprintf('- Name: `%s`', $callable[1]);
  279. $string .= "\n".sprintf('- Class: `%s`', $callable[0]);
  280. $string .= "\n- Static: yes";
  281. } else {
  282. $string .= "\n".sprintf('- Name: `%s`', substr($callable[1], 8));
  283. $string .= "\n".sprintf('- Class: `%s`', $callable[0]);
  284. $string .= "\n- Static: yes";
  285. $string .= "\n- Parent: yes";
  286. }
  287. }
  288. return $this->write($string."\n");
  289. }
  290. if (\is_string($callable)) {
  291. $string .= "\n- Type: `function`";
  292. if (false === strpos($callable, '::')) {
  293. $string .= "\n".sprintf('- Name: `%s`', $callable);
  294. } else {
  295. $callableParts = explode('::', $callable);
  296. $string .= "\n".sprintf('- Name: `%s`', $callableParts[1]);
  297. $string .= "\n".sprintf('- Class: `%s`', $callableParts[0]);
  298. $string .= "\n- Static: yes";
  299. }
  300. return $this->write($string."\n");
  301. }
  302. if ($callable instanceof \Closure) {
  303. $string .= "\n- Type: `closure`";
  304. $r = new \ReflectionFunction($callable);
  305. if (false !== strpos($r->name, '{closure}')) {
  306. return $this->write($string."\n");
  307. }
  308. $string .= "\n".sprintf('- Name: `%s`', $r->name);
  309. if ($class = $r->getClosureScopeClass()) {
  310. $string .= "\n".sprintf('- Class: `%s`', $class->name);
  311. if (!$r->getClosureThis()) {
  312. $string .= "\n- Static: yes";
  313. }
  314. }
  315. return $this->write($string."\n");
  316. }
  317. if (method_exists($callable, '__invoke')) {
  318. $string .= "\n- Type: `object`";
  319. $string .= "\n".sprintf('- Name: `%s`', \get_class($callable));
  320. return $this->write($string."\n");
  321. }
  322. throw new \InvalidArgumentException('Callable is not describable.');
  323. }
  324. private function formatRouterConfig(array $array): string
  325. {
  326. if (!$array) {
  327. return 'NONE';
  328. }
  329. $string = '';
  330. ksort($array);
  331. foreach ($array as $name => $value) {
  332. $string .= "\n".' - `'.$name.'`: '.$this->formatValue($value);
  333. }
  334. return $string;
  335. }
  336. }