TextDescriptor.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  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\Formatter\OutputFormatter;
  12. use Symfony\Component\Console\Helper\Dumper;
  13. use Symfony\Component\Console\Helper\Table;
  14. use Symfony\Component\Console\Style\SymfonyStyle;
  15. use Symfony\Component\DependencyInjection\Alias;
  16. use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
  17. use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
  18. use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
  19. use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
  20. use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
  21. use Symfony\Component\DependencyInjection\ContainerBuilder;
  22. use Symfony\Component\DependencyInjection\Definition;
  23. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
  24. use Symfony\Component\DependencyInjection\Reference;
  25. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  26. use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
  27. use Symfony\Component\Routing\Route;
  28. use Symfony\Component\Routing\RouteCollection;
  29. /**
  30. * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  31. *
  32. * @internal
  33. */
  34. class TextDescriptor extends Descriptor
  35. {
  36. private $fileLinkFormatter;
  37. public function __construct(FileLinkFormatter $fileLinkFormatter = null)
  38. {
  39. $this->fileLinkFormatter = $fileLinkFormatter;
  40. }
  41. protected function describeRouteCollection(RouteCollection $routes, array $options = [])
  42. {
  43. $showControllers = isset($options['show_controllers']) && $options['show_controllers'];
  44. $tableHeaders = ['Name', 'Method', 'Scheme', 'Host', 'Path'];
  45. if ($showControllers) {
  46. $tableHeaders[] = 'Controller';
  47. }
  48. $tableRows = [];
  49. foreach ($routes->all() as $name => $route) {
  50. $controller = $route->getDefault('_controller');
  51. $row = [
  52. $name,
  53. $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY',
  54. $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY',
  55. '' !== $route->getHost() ? $route->getHost() : 'ANY',
  56. $this->formatControllerLink($controller, $route->getPath(), $options['container'] ?? null),
  57. ];
  58. if ($showControllers) {
  59. $row[] = $controller ? $this->formatControllerLink($controller, $this->formatCallable($controller), $options['container'] ?? null) : '';
  60. }
  61. $tableRows[] = $row;
  62. }
  63. if (isset($options['output'])) {
  64. $options['output']->table($tableHeaders, $tableRows);
  65. } else {
  66. $table = new Table($this->getOutput());
  67. $table->setHeaders($tableHeaders)->setRows($tableRows);
  68. $table->render();
  69. }
  70. }
  71. protected function describeRoute(Route $route, array $options = [])
  72. {
  73. $defaults = $route->getDefaults();
  74. if (isset($defaults['_controller'])) {
  75. $defaults['_controller'] = $this->formatControllerLink($defaults['_controller'], $this->formatCallable($defaults['_controller']), $options['container'] ?? null);
  76. }
  77. $tableHeaders = ['Property', 'Value'];
  78. $tableRows = [
  79. ['Route Name', $options['name'] ?? ''],
  80. ['Path', $route->getPath()],
  81. ['Path Regex', $route->compile()->getRegex()],
  82. ['Host', ('' !== $route->getHost() ? $route->getHost() : 'ANY')],
  83. ['Host Regex', ('' !== $route->getHost() ? $route->compile()->getHostRegex() : '')],
  84. ['Scheme', ($route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY')],
  85. ['Method', ($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY')],
  86. ['Requirements', ($route->getRequirements() ? $this->formatRouterConfig($route->getRequirements()) : 'NO CUSTOM')],
  87. ['Class', \get_class($route)],
  88. ['Defaults', $this->formatRouterConfig($defaults)],
  89. ['Options', $this->formatRouterConfig($route->getOptions())],
  90. ];
  91. if ('' !== $route->getCondition()) {
  92. $tableRows[] = ['Condition', $route->getCondition()];
  93. }
  94. $table = new Table($this->getOutput());
  95. $table->setHeaders($tableHeaders)->setRows($tableRows);
  96. $table->render();
  97. }
  98. protected function describeContainerParameters(ParameterBag $parameters, array $options = [])
  99. {
  100. $tableHeaders = ['Parameter', 'Value'];
  101. $tableRows = [];
  102. foreach ($this->sortParameters($parameters) as $parameter => $value) {
  103. $tableRows[] = [$parameter, $this->formatParameter($value)];
  104. }
  105. $options['output']->title('Symfony Container Parameters');
  106. $options['output']->table($tableHeaders, $tableRows);
  107. }
  108. protected function describeContainerTags(ContainerBuilder $builder, array $options = [])
  109. {
  110. $showHidden = isset($options['show_hidden']) && $options['show_hidden'];
  111. if ($showHidden) {
  112. $options['output']->title('Symfony Container Hidden Tags');
  113. } else {
  114. $options['output']->title('Symfony Container Tags');
  115. }
  116. foreach ($this->findDefinitionsByTag($builder, $showHidden) as $tag => $definitions) {
  117. $options['output']->section(sprintf('"%s" tag', $tag));
  118. $options['output']->listing(array_keys($definitions));
  119. }
  120. }
  121. protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null)
  122. {
  123. if (!isset($options['id'])) {
  124. throw new \InvalidArgumentException('An "id" option must be provided.');
  125. }
  126. if ($service instanceof Alias) {
  127. $this->describeContainerAlias($service, $options, $builder);
  128. } elseif ($service instanceof Definition) {
  129. $this->describeContainerDefinition($service, $options);
  130. } else {
  131. $options['output']->title(sprintf('Information for Service "<info>%s</info>"', $options['id']));
  132. $options['output']->table(
  133. ['Service ID', 'Class'],
  134. [
  135. [$options['id'] ?? '-', \get_class($service)],
  136. ]
  137. );
  138. }
  139. }
  140. protected function describeContainerServices(ContainerBuilder $builder, array $options = [])
  141. {
  142. $showHidden = isset($options['show_hidden']) && $options['show_hidden'];
  143. $showTag = $options['tag'] ?? null;
  144. if ($showHidden) {
  145. $title = 'Symfony Container Hidden Services';
  146. } else {
  147. $title = 'Symfony Container Services';
  148. }
  149. if ($showTag) {
  150. $title .= sprintf(' Tagged with "%s" Tag', $options['tag']);
  151. }
  152. $options['output']->title($title);
  153. $serviceIds = isset($options['tag']) && $options['tag']
  154. ? $this->sortTaggedServicesByPriority($builder->findTaggedServiceIds($options['tag']))
  155. : $this->sortServiceIds($builder->getServiceIds());
  156. $maxTags = [];
  157. if (isset($options['filter'])) {
  158. $serviceIds = array_filter($serviceIds, $options['filter']);
  159. }
  160. foreach ($serviceIds as $key => $serviceId) {
  161. $definition = $this->resolveServiceDefinition($builder, $serviceId);
  162. // filter out hidden services unless shown explicitly
  163. if ($showHidden xor '.' === ($serviceId[0] ?? null)) {
  164. unset($serviceIds[$key]);
  165. continue;
  166. }
  167. if ($definition instanceof Definition) {
  168. if ($showTag) {
  169. $tags = $definition->getTag($showTag);
  170. foreach ($tags as $tag) {
  171. foreach ($tag as $key => $value) {
  172. if (!isset($maxTags[$key])) {
  173. $maxTags[$key] = \strlen($key);
  174. }
  175. if (\strlen($value) > $maxTags[$key]) {
  176. $maxTags[$key] = \strlen($value);
  177. }
  178. }
  179. }
  180. }
  181. }
  182. }
  183. $tagsCount = \count($maxTags);
  184. $tagsNames = array_keys($maxTags);
  185. $tableHeaders = array_merge(['Service ID'], $tagsNames, ['Class name']);
  186. $tableRows = [];
  187. $rawOutput = isset($options['raw_text']) && $options['raw_text'];
  188. foreach ($serviceIds as $serviceId) {
  189. $definition = $this->resolveServiceDefinition($builder, $serviceId);
  190. $styledServiceId = $rawOutput ? $serviceId : sprintf('<fg=cyan>%s</fg=cyan>', OutputFormatter::escape($serviceId));
  191. if ($definition instanceof Definition) {
  192. if ($showTag) {
  193. foreach ($this->sortByPriority($definition->getTag($showTag)) as $key => $tag) {
  194. $tagValues = [];
  195. foreach ($tagsNames as $tagName) {
  196. $tagValues[] = $tag[$tagName] ?? '';
  197. }
  198. if (0 === $key) {
  199. $tableRows[] = array_merge([$serviceId], $tagValues, [$definition->getClass()]);
  200. } else {
  201. $tableRows[] = array_merge([' "'], $tagValues, ['']);
  202. }
  203. }
  204. } else {
  205. $tableRows[] = [$styledServiceId, $definition->getClass()];
  206. }
  207. } elseif ($definition instanceof Alias) {
  208. $alias = $definition;
  209. $tableRows[] = array_merge([$styledServiceId, sprintf('alias for "%s"', $alias)], $tagsCount ? array_fill(0, $tagsCount, '') : []);
  210. } else {
  211. $tableRows[] = array_merge([$styledServiceId, \get_class($definition)], $tagsCount ? array_fill(0, $tagsCount, '') : []);
  212. }
  213. }
  214. $options['output']->table($tableHeaders, $tableRows);
  215. }
  216. protected function describeContainerDefinition(Definition $definition, array $options = [])
  217. {
  218. if (isset($options['id'])) {
  219. $options['output']->title(sprintf('Information for Service "<info>%s</info>"', $options['id']));
  220. }
  221. if ('' !== $classDescription = $this->getClassDescription((string) $definition->getClass())) {
  222. $options['output']->text($classDescription."\n");
  223. }
  224. $tableHeaders = ['Option', 'Value'];
  225. $tableRows[] = ['Service ID', $options['id'] ?? '-'];
  226. $tableRows[] = ['Class', $definition->getClass() ?: '-'];
  227. $omitTags = isset($options['omit_tags']) && $options['omit_tags'];
  228. if (!$omitTags && ($tags = $definition->getTags())) {
  229. $tagInformation = [];
  230. foreach ($tags as $tagName => $tagData) {
  231. foreach ($tagData as $tagParameters) {
  232. $parameters = array_map(function ($key, $value) {
  233. return sprintf('<info>%s</info>: %s', $key, $value);
  234. }, array_keys($tagParameters), array_values($tagParameters));
  235. $parameters = implode(', ', $parameters);
  236. if ('' === $parameters) {
  237. $tagInformation[] = sprintf('%s', $tagName);
  238. } else {
  239. $tagInformation[] = sprintf('%s (%s)', $tagName, $parameters);
  240. }
  241. }
  242. }
  243. $tagInformation = implode("\n", $tagInformation);
  244. } else {
  245. $tagInformation = '-';
  246. }
  247. $tableRows[] = ['Tags', $tagInformation];
  248. $calls = $definition->getMethodCalls();
  249. if (\count($calls) > 0) {
  250. $callInformation = [];
  251. foreach ($calls as $call) {
  252. $callInformation[] = $call[0];
  253. }
  254. $tableRows[] = ['Calls', implode(', ', $callInformation)];
  255. }
  256. $tableRows[] = ['Public', $definition->isPublic() && !$definition->isPrivate() ? 'yes' : 'no'];
  257. $tableRows[] = ['Synthetic', $definition->isSynthetic() ? 'yes' : 'no'];
  258. $tableRows[] = ['Lazy', $definition->isLazy() ? 'yes' : 'no'];
  259. $tableRows[] = ['Shared', $definition->isShared() ? 'yes' : 'no'];
  260. $tableRows[] = ['Abstract', $definition->isAbstract() ? 'yes' : 'no'];
  261. $tableRows[] = ['Autowired', $definition->isAutowired() ? 'yes' : 'no'];
  262. $tableRows[] = ['Autoconfigured', $definition->isAutoconfigured() ? 'yes' : 'no'];
  263. if ($definition->getFile()) {
  264. $tableRows[] = ['Required File', $definition->getFile() ?: '-'];
  265. }
  266. if ($factory = $definition->getFactory()) {
  267. if (\is_array($factory)) {
  268. if ($factory[0] instanceof Reference) {
  269. $tableRows[] = ['Factory Service', $factory[0]];
  270. } elseif ($factory[0] instanceof Definition) {
  271. throw new \InvalidArgumentException('Factory is not describable.');
  272. } else {
  273. $tableRows[] = ['Factory Class', $factory[0]];
  274. }
  275. $tableRows[] = ['Factory Method', $factory[1]];
  276. } else {
  277. $tableRows[] = ['Factory Function', $factory];
  278. }
  279. }
  280. $showArguments = isset($options['show_arguments']) && $options['show_arguments'];
  281. $argumentsInformation = [];
  282. if ($showArguments && ($arguments = $definition->getArguments())) {
  283. foreach ($arguments as $argument) {
  284. if ($argument instanceof ServiceClosureArgument) {
  285. $argument = $argument->getValues()[0];
  286. }
  287. if ($argument instanceof Reference) {
  288. $argumentsInformation[] = sprintf('Service(%s)', (string) $argument);
  289. } elseif ($argument instanceof IteratorArgument) {
  290. if ($argument instanceof TaggedIteratorArgument) {
  291. $argumentsInformation[] = sprintf('Tagged Iterator for "%s"%s', $argument->getTag(), $options['is_debug'] ? '' : sprintf(' (%d element(s))', \count($argument->getValues())));
  292. } else {
  293. $argumentsInformation[] = sprintf('Iterator (%d element(s))', \count($argument->getValues()));
  294. }
  295. foreach ($argument->getValues() as $ref) {
  296. $argumentsInformation[] = sprintf('- Service(%s)', $ref);
  297. }
  298. } elseif ($argument instanceof ServiceLocatorArgument) {
  299. $argumentsInformation[] = sprintf('Service locator (%d element(s))', \count($argument->getValues()));
  300. } elseif ($argument instanceof Definition) {
  301. $argumentsInformation[] = 'Inlined Service';
  302. } elseif ($argument instanceof AbstractArgument) {
  303. $argumentsInformation[] = sprintf('Abstract argument (%s)', $argument->getText());
  304. } else {
  305. $argumentsInformation[] = \is_array($argument) ? sprintf('Array (%d element(s))', \count($argument)) : $argument;
  306. }
  307. }
  308. $tableRows[] = ['Arguments', implode("\n", $argumentsInformation)];
  309. }
  310. $options['output']->table($tableHeaders, $tableRows);
  311. }
  312. protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void
  313. {
  314. $containerDeprecationFilePath = sprintf('%s/%sDeprecations.log', $builder->getParameter('kernel.build_dir'), $builder->getParameter('kernel.container_class'));
  315. if (!file_exists($containerDeprecationFilePath)) {
  316. $options['output']->warning('The deprecation file does not exist, please try warming the cache first.');
  317. return;
  318. }
  319. $logs = unserialize(file_get_contents($containerDeprecationFilePath));
  320. if (0 === \count($logs)) {
  321. $options['output']->success('There are no deprecations in the logs!');
  322. return;
  323. }
  324. $formattedLogs = [];
  325. $remainingCount = 0;
  326. foreach ($logs as $log) {
  327. $formattedLogs[] = sprintf("%sx: %s\n in %s:%s", $log['count'], $log['message'], $log['file'], $log['line']);
  328. $remainingCount += $log['count'];
  329. }
  330. $options['output']->title(sprintf('Remaining deprecations (%s)', $remainingCount));
  331. $options['output']->listing($formattedLogs);
  332. }
  333. protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null)
  334. {
  335. if ($alias->isPublic() && !$alias->isPrivate()) {
  336. $options['output']->comment(sprintf('This service is a <info>public</info> alias for the service <info>%s</info>', (string) $alias));
  337. } else {
  338. $options['output']->comment(sprintf('This service is a <comment>private</comment> alias for the service <info>%s</info>', (string) $alias));
  339. }
  340. if (!$builder) {
  341. return;
  342. }
  343. $this->describeContainerDefinition($builder->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias]));
  344. }
  345. protected function describeContainerParameter($parameter, array $options = [])
  346. {
  347. $options['output']->table(
  348. ['Parameter', 'Value'],
  349. [
  350. [$options['parameter'], $this->formatParameter($parameter),
  351. ],
  352. ]);
  353. }
  354. protected function describeContainerEnvVars(array $envs, array $options = [])
  355. {
  356. $dump = new Dumper($this->output);
  357. $options['output']->title('Symfony Container Environment Variables');
  358. if (null !== $name = $options['name'] ?? null) {
  359. $options['output']->comment('Displaying detailed environment variable usage matching '.$name);
  360. $matches = false;
  361. foreach ($envs as $env) {
  362. if ($name === $env['name'] || false !== stripos($env['name'], $name)) {
  363. $matches = true;
  364. $options['output']->section('%env('.$env['processor'].':'.$env['name'].')%');
  365. $options['output']->table([], [
  366. ['<info>Default value</>', $env['default_available'] ? $dump($env['default_value']) : 'n/a'],
  367. ['<info>Real value</>', $env['runtime_available'] ? $dump($env['runtime_value']) : 'n/a'],
  368. ['<info>Processed value</>', $env['default_available'] || $env['runtime_available'] ? $dump($env['processed_value']) : 'n/a'],
  369. ]);
  370. }
  371. }
  372. if (!$matches) {
  373. $options['output']->block('None of the environment variables match this name.');
  374. } else {
  375. $options['output']->comment('Note real values might be different between web and CLI.');
  376. }
  377. return;
  378. }
  379. if (!$envs) {
  380. $options['output']->block('No environment variables are being used.');
  381. return;
  382. }
  383. $rows = [];
  384. $missing = [];
  385. foreach ($envs as $env) {
  386. if (isset($rows[$env['name']])) {
  387. continue;
  388. }
  389. $rows[$env['name']] = [
  390. $env['name'],
  391. $env['default_available'] ? $dump($env['default_value']) : 'n/a',
  392. $env['runtime_available'] ? $dump($env['runtime_value']) : 'n/a',
  393. ];
  394. if (!$env['default_available'] && !$env['runtime_available']) {
  395. $missing[$env['name']] = true;
  396. }
  397. }
  398. $options['output']->table(['Name', 'Default value', 'Real value'], $rows);
  399. $options['output']->comment('Note real values might be different between web and CLI.');
  400. if ($missing) {
  401. $options['output']->warning('The following variables are missing:');
  402. $options['output']->listing(array_keys($missing));
  403. }
  404. }
  405. protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = [])
  406. {
  407. $event = \array_key_exists('event', $options) ? $options['event'] : null;
  408. if (null !== $event) {
  409. $title = sprintf('Registered Listeners for "%s" Event', $event);
  410. } else {
  411. $title = 'Registered Listeners Grouped by Event';
  412. }
  413. $options['output']->title($title);
  414. $registeredListeners = $eventDispatcher->getListeners($event);
  415. if (null !== $event) {
  416. $this->renderEventListenerTable($eventDispatcher, $event, $registeredListeners, $options['output']);
  417. } else {
  418. ksort($registeredListeners);
  419. foreach ($registeredListeners as $eventListened => $eventListeners) {
  420. $options['output']->section(sprintf('"%s" event', $eventListened));
  421. $this->renderEventListenerTable($eventDispatcher, $eventListened, $eventListeners, $options['output']);
  422. }
  423. }
  424. }
  425. protected function describeCallable($callable, array $options = [])
  426. {
  427. $this->writeText($this->formatCallable($callable), $options);
  428. }
  429. private function renderEventListenerTable(EventDispatcherInterface $eventDispatcher, string $event, array $eventListeners, SymfonyStyle $io)
  430. {
  431. $tableHeaders = ['Order', 'Callable', 'Priority'];
  432. $tableRows = [];
  433. foreach ($eventListeners as $order => $listener) {
  434. $tableRows[] = [sprintf('#%d', $order + 1), $this->formatCallable($listener), $eventDispatcher->getListenerPriority($event, $listener)];
  435. }
  436. $io->table($tableHeaders, $tableRows);
  437. }
  438. private function formatRouterConfig(array $config): string
  439. {
  440. if (empty($config)) {
  441. return 'NONE';
  442. }
  443. ksort($config);
  444. $configAsString = '';
  445. foreach ($config as $key => $value) {
  446. $configAsString .= sprintf("\n%s: %s", $key, $this->formatValue($value));
  447. }
  448. return trim($configAsString);
  449. }
  450. private function formatControllerLink($controller, string $anchorText, callable $getContainer = null): string
  451. {
  452. if (null === $this->fileLinkFormatter) {
  453. return $anchorText;
  454. }
  455. try {
  456. if (null === $controller) {
  457. return $anchorText;
  458. } elseif (\is_array($controller)) {
  459. $r = new \ReflectionMethod($controller[0], $controller[1]);
  460. } elseif ($controller instanceof \Closure) {
  461. $r = new \ReflectionFunction($controller);
  462. } elseif (method_exists($controller, '__invoke')) {
  463. $r = new \ReflectionMethod($controller, '__invoke');
  464. } elseif (!\is_string($controller)) {
  465. return $anchorText;
  466. } elseif (false !== strpos($controller, '::')) {
  467. $r = new \ReflectionMethod($controller);
  468. } else {
  469. $r = new \ReflectionFunction($controller);
  470. }
  471. } catch (\ReflectionException $e) {
  472. $id = $controller;
  473. $method = '__invoke';
  474. if ($pos = strpos($controller, '::')) {
  475. $id = substr($controller, 0, $pos);
  476. $method = substr($controller, $pos + 2);
  477. }
  478. if (!$getContainer || !($container = $getContainer()) || !$container->has($id)) {
  479. return $anchorText;
  480. }
  481. try {
  482. $r = new \ReflectionMethod($container->findDefinition($id)->getClass(), $method);
  483. } catch (\ReflectionException $e) {
  484. return $anchorText;
  485. }
  486. }
  487. $fileLink = $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine());
  488. if ($fileLink) {
  489. return sprintf('<href=%s>%s</>', $fileLink, $anchorText);
  490. }
  491. return $anchorText;
  492. }
  493. private function formatCallable($callable): string
  494. {
  495. if (\is_array($callable)) {
  496. if (\is_object($callable[0])) {
  497. return sprintf('%s::%s()', \get_class($callable[0]), $callable[1]);
  498. }
  499. return sprintf('%s::%s()', $callable[0], $callable[1]);
  500. }
  501. if (\is_string($callable)) {
  502. return sprintf('%s()', $callable);
  503. }
  504. if ($callable instanceof \Closure) {
  505. $r = new \ReflectionFunction($callable);
  506. if (false !== strpos($r->name, '{closure}')) {
  507. return 'Closure()';
  508. }
  509. if ($class = $r->getClosureScopeClass()) {
  510. return sprintf('%s::%s()', $class->name, $r->name);
  511. }
  512. return $r->name.'()';
  513. }
  514. if (method_exists($callable, '__invoke')) {
  515. return sprintf('%s::__invoke()', \get_class($callable));
  516. }
  517. throw new \InvalidArgumentException('Callable is not describable.');
  518. }
  519. private function writeText(string $content, array $options = [])
  520. {
  521. $this->write(
  522. isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
  523. isset($options['raw_output']) ? !$options['raw_output'] : true
  524. );
  525. }
  526. }