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 12namespace Symfony\Bundle\FrameworkBundle\Command; 13 14use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; 15use Symfony\Component\Config\ConfigCache; 16use Symfony\Component\Config\FileLocator; 17use Symfony\Component\Console\Exception\InvalidArgumentException; 18use Symfony\Component\Console\Input\InputArgument; 19use Symfony\Component\Console\Input\InputInterface; 20use Symfony\Component\Console\Input\InputOption; 21use Symfony\Component\Console\Output\OutputInterface; 22use Symfony\Component\Console\Style\SymfonyStyle; 23use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; 24use Symfony\Component\DependencyInjection\ContainerBuilder; 25use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 26use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; 27 28/** 29 * A console command for retrieving information about services. 30 * 31 * @author Ryan Weaver <ryan@thatsquality.com> 32 * 33 * @internal since version 3.4 34 */ 35class ContainerDebugCommand extends ContainerAwareCommand 36{ 37 protected static $defaultName = 'debug:container'; 38 39 /** 40 * @var ContainerBuilder|null 41 */ 42 protected $containerBuilder; 43 44 /** 45 * {@inheritdoc} 46 */ 47 protected function configure() 48 { 49 $this 50 ->setDefinition([ 51 new InputArgument('name', InputArgument::OPTIONAL, 'A service name (foo)'), 52 new InputOption('show-private', null, InputOption::VALUE_NONE, 'Used to show public *and* private services'), 53 new InputOption('show-arguments', null, InputOption::VALUE_NONE, 'Used to show arguments in services'), 54 new InputOption('tag', null, InputOption::VALUE_REQUIRED, 'Shows all services with a specific tag'), 55 new InputOption('tags', null, InputOption::VALUE_NONE, 'Displays tagged services for an application'), 56 new InputOption('parameter', null, InputOption::VALUE_REQUIRED, 'Displays a specific parameter for an application'), 57 new InputOption('parameters', null, InputOption::VALUE_NONE, 'Displays parameters for an application'), 58 new InputOption('types', null, InputOption::VALUE_NONE, 'Displays types (classes/interfaces) available in the container'), 59 new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), 60 new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), 61 ]) 62 ->setDescription('Displays current services for an application') 63 ->setHelp(<<<'EOF' 64The <info>%command.name%</info> command displays all configured <comment>public</comment> services: 65 66 <info>php %command.full_name%</info> 67 68To get specific information about a service, specify its name: 69 70 <info>php %command.full_name% validator</info> 71 72To see available types that can be used for autowiring, use the <info>--types</info> flag: 73 74 <info>php %command.full_name% --types</info> 75 76By default, private services are hidden. You can display all services by 77using the <info>--show-private</info> flag: 78 79 <info>php %command.full_name% --show-private</info> 80 81Use the --tags option to display tagged <comment>public</comment> services grouped by tag: 82 83 <info>php %command.full_name% --tags</info> 84 85Find all services with a specific tag by specifying the tag name with the <info>--tag</info> option: 86 87 <info>php %command.full_name% --tag=form.type</info> 88 89Use the <info>--parameters</info> option to display all parameters: 90 91 <info>php %command.full_name% --parameters</info> 92 93Display a specific parameter by specifying its name with the <info>--parameter</info> option: 94 95 <info>php %command.full_name% --parameter=kernel.debug</info> 96 97EOF 98 ) 99 ; 100 } 101 102 /** 103 * {@inheritdoc} 104 */ 105 protected function execute(InputInterface $input, OutputInterface $output) 106 { 107 $io = new SymfonyStyle($input, $output); 108 $errorIo = $io->getErrorStyle(); 109 110 $this->validateInput($input); 111 $object = $this->getContainerBuilder(); 112 113 if ($input->getOption('types')) { 114 $options = ['show_private' => true]; 115 $options['filter'] = [$this, 'filterToServiceTypes']; 116 } elseif ($input->getOption('parameters')) { 117 $parameters = []; 118 foreach ($object->getParameterBag()->all() as $k => $v) { 119 $parameters[$k] = $object->resolveEnvPlaceholders($v); 120 } 121 $object = new ParameterBag($parameters); 122 $options = []; 123 } elseif ($parameter = $input->getOption('parameter')) { 124 $options = ['parameter' => $parameter]; 125 } elseif ($input->getOption('tags')) { 126 $options = ['group_by' => 'tags', 'show_private' => $input->getOption('show-private')]; 127 } elseif ($tag = $input->getOption('tag')) { 128 $options = ['tag' => $tag, 'show_private' => $input->getOption('show-private')]; 129 } elseif ($name = $input->getArgument('name')) { 130 $name = $this->findProperServiceName($input, $errorIo, $object, $name); 131 $options = ['id' => $name]; 132 } else { 133 $options = ['show_private' => $input->getOption('show-private')]; 134 } 135 136 $helper = new DescriptorHelper(); 137 $options['format'] = $input->getOption('format'); 138 $options['show_arguments'] = $input->getOption('show-arguments'); 139 $options['raw_text'] = $input->getOption('raw'); 140 $options['output'] = $io; 141 $options['is_debug'] = $this->getApplication()->getKernel()->isDebug(); 142 $helper->describe($io, $object, $options); 143 144 if (!$input->getArgument('name') && !$input->getOption('tag') && !$input->getOption('parameter') && $input->isInteractive()) { 145 if ($input->getOption('tags')) { 146 $errorIo->comment('To search for a specific tag, re-run this command with a search term. (e.g. <comment>debug:container --tag=form.type</comment>)'); 147 } elseif ($input->getOption('parameters')) { 148 $errorIo->comment('To search for a specific parameter, re-run this command with a search term. (e.g. <comment>debug:container --parameter=kernel.debug</comment>)'); 149 } else { 150 $errorIo->comment('To search for a specific service, re-run this command with a search term. (e.g. <comment>debug:container log</comment>)'); 151 } 152 } 153 } 154 155 /** 156 * Validates input arguments and options. 157 * 158 * @throws \InvalidArgumentException 159 */ 160 protected function validateInput(InputInterface $input) 161 { 162 $options = ['tags', 'tag', 'parameters', 'parameter']; 163 164 $optionsCount = 0; 165 foreach ($options as $option) { 166 if ($input->getOption($option)) { 167 ++$optionsCount; 168 } 169 } 170 171 $name = $input->getArgument('name'); 172 if ((null !== $name) && ($optionsCount > 0)) { 173 throw new InvalidArgumentException('The options tags, tag, parameters & parameter can not be combined with the service name argument.'); 174 } elseif ((null === $name) && $optionsCount > 1) { 175 throw new InvalidArgumentException('The options tags, tag, parameters & parameter can not be combined together.'); 176 } 177 } 178 179 /** 180 * Loads the ContainerBuilder from the cache. 181 * 182 * @return ContainerBuilder 183 * 184 * @throws \LogicException 185 */ 186 protected function getContainerBuilder() 187 { 188 if ($this->containerBuilder) { 189 return $this->containerBuilder; 190 } 191 192 $kernel = $this->getApplication()->getKernel(); 193 194 if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { 195 $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); 196 $container = $buildContainer(); 197 $container->getCompilerPassConfig()->setRemovingPasses([]); 198 $container->getCompilerPassConfig()->setAfterRemovingPasses([]); 199 $container->compile(); 200 } else { 201 (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); 202 $locatorPass = new ServiceLocatorTagPass(); 203 $locatorPass->process($container); 204 } 205 206 return $this->containerBuilder = $container; 207 } 208 209 private function findProperServiceName(InputInterface $input, SymfonyStyle $io, ContainerBuilder $builder, $name) 210 { 211 if ($builder->has($name) || !$input->isInteractive()) { 212 return $name; 213 } 214 215 $matchingServices = $this->findServiceIdsContaining($builder, $name); 216 if (empty($matchingServices)) { 217 throw new InvalidArgumentException(sprintf('No services found that match "%s".', $name)); 218 } 219 220 $default = 1 === \count($matchingServices) ? $matchingServices[0] : null; 221 222 return $io->choice('Select one of the following services to display its information', $matchingServices, $default); 223 } 224 225 private function findServiceIdsContaining(ContainerBuilder $builder, $name) 226 { 227 $serviceIds = $builder->getServiceIds(); 228 $foundServiceIds = []; 229 foreach ($serviceIds as $serviceId) { 230 if (false === stripos($serviceId, $name)) { 231 continue; 232 } 233 $foundServiceIds[] = $serviceId; 234 } 235 236 return $foundServiceIds; 237 } 238 239 /** 240 * @internal 241 */ 242 public function filterToServiceTypes($serviceId) 243 { 244 // filter out things that could not be valid class names 245 if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $serviceId)) { 246 return false; 247 } 248 249 // if the id has a \, assume it is a class 250 if (false !== strpos($serviceId, '\\')) { 251 return true; 252 } 253 254 try { 255 new \ReflectionClass($serviceId); 256 257 return true; 258 } catch (\ReflectionException $e) { 259 // the service id is not a valid class/interface 260 return false; 261 } 262 } 263} 264