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