diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php new file mode 100644 index 0000000000..b91a20cc6d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -0,0 +1,288 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\Output; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Definition; + +/** + * A console command for retrieving information about services + * + * @author Ryan Weaver + */ +class ContainerDebugCommand extends Command +{ + /** + * @var \Symfony\Component\DependencyInjection\ContainerBuilder + */ + protected $containerBuilder; + + /** + * @see Command + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('name', InputArgument::OPTIONAL, 'A service name (foo) or search (foo*)'), + new InputOption('show-private', null, InputOption::VALUE_NONE, 'Use to show public *and* private services'), + )) + ->setName('container:debug') + ->setDescription('Displays current services for an application') + ->setHelp(<<container:debug displays all configured public services: + + container:debug + +You can also search for specific services using wildcards (*): + + container:debug doctrine.* + + container:debug *event_manager + +To get specific information about a service, use specify its name exactly: + + container:debug validator + +By default, private services are hidden. You can display all services by +using the --show-private flag: + + container:debug --show-private +EOF + ) + ; + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $filter = $input->getArgument('name'); + + $this->containerBuilder = $this->getContainerBuilder(); + $serviceIds = $this->filterServices($this->containerBuilder->getServiceIds(), $filter); + + if (1 == count($serviceIds) && false === strpos($filter, '*')) { + $this->outputService($output, $serviceIds[0]); + } else { + $showPrivate = $input->getOption('show-private'); + $this->outputServices($output, $serviceIds, $filter, $showPrivate); + } + } + + protected function outputServices(OutputInterface $output, $serviceIds, $filter, $showPrivate = false) + { + // set the label to specify public or public+private + if ($showPrivate) { + $label = 'Public and private services'; + } else { + $label = 'Public services'; + } + + if ($filter) { + $label .= sprintf(' matching %s', $filter); + } + $output->writeln($this->getHelper('formatter')->formatSection('container', $label)); + + // loop through to find get space needed and filter private services + $maxName = 4; + $maxScope = 6; + foreach ($serviceIds as $key => $serviceId) { + $definition = $this->resolveServiceDefinition($serviceId); + + if ($definition instanceof Definition) { + // filter out private services unless shown explicitly + if (!$showPrivate && !$definition->isPublic()) { + unset($serviceIds[$key]); + continue; + } + + if (strlen($definition->getScope()) > $maxScope) { + $maxScope = strlen($definition->getScope()); + } + } + + if (strlen($serviceId) > $maxName) { + $maxName = strlen($serviceId); + } + } + $format = '%-'.$maxName.'s %-'.$maxScope.'s %s'; + + // the title field needs extra space to make up for comment tags + $format1 = '%-'.($maxName + 19).'s %-'.($maxScope + 19).'s %s'; + $output->writeln(sprintf($format1, 'Name', 'Scope', 'Class Name')); + + foreach ($serviceIds as $serviceId) { + $definition = $this->resolveServiceDefinition($serviceId); + + if ($definition instanceof Definition) { + $output->writeln(sprintf($format, $serviceId, $definition->getScope(), $definition->getClass())); + } elseif ($definition instanceof Alias) { + $alias = $definition; + $output->writeln(sprintf($format, $serviceId, sprintf('--> Alias for service %s', (string) $alias), '')); + } else { + // we have no information (happens with "service_container") + $service = $definition; + $output->writeln(sprintf($format, $serviceId, '', get_class($service))); + } + } + } + + /** + * Renders detailed service information about one service + */ + protected function outputService(OutputInterface $output, $serviceId) + { + $definition = $this->resolveServiceDefinition($serviceId); + + $label = sprintf('Information for service %s', $serviceId); + $output->writeln($this->getHelper('formatter')->formatSection('container', $label)); + $output->writeln(''); + + if ($definition instanceof Definition) { + $output->writeln(sprintf('Service Id %s', $serviceId)); + $output->writeln(sprintf('Class %s', $definition->getClass())); + + $tags = $definition->getTags() ? implode(', ', array_keys($definition->getTags())) : 'no tags'; + $output->writeln(sprintf('Tags %s', $tags)); + + $output->writeln(sprintf('Scope %s', $definition->getScope())); + + $public = $definition->isPublic() ? 'public' : 'private'; + $output->writeln(sprintf('Public %s', $public)); + } elseif ($definition instanceof Alias) { + $alias = $definition; + $output->writeln(sprintf('This service is an alias for the service %s', (string) $alias)); + } else { + // edge case (but true for "service_container", all we have is the service itself + $service = $definition; + $output->writeln(sprintf('Service Id %s', $serviceId)); + $output->writeln(sprintf('Class %s', get_class($service))); + } + } + + /** + * Loads the ContainerBuilder from the cache. + * + * @see ContainerBuilderDebugDumpPass + * @return \Symfony\Component\DependencyInjection\ContainerBuilder + */ + private function getContainerBuilder() + { + $cachedFile = ContainerBuilderDebugDumpPass::getBuilderCacheFilename($this->container); + + if (!file_exists($cachedFile)) { + throw new \LogicException(sprintf('Debug information about the container could not be found. Please clear the cache and try again.')); + } + + return unserialize(file_get_contents($cachedFile)); + } + + /** + * Given an array of service IDs, this returns the array of corresponding + * Definition and Alias objects that those ids represent. + * + * @param string $serviceId The service id to resolve + * @return \Symfony\Component\DependencyInjection\Definition|\Symfony\Component\DependencyInjection\Alias + */ + private function resolveServiceDefinition($serviceId) + { + if ($this->containerBuilder->hasDefinition($serviceId)) { + return $this->containerBuilder->getDefinition($serviceId); + } + + // Some service IDs don't have a Definition, they're simply an Alias + if ($this->containerBuilder->hasAlias($serviceId)) { + return $this->containerBuilder->getAlias($serviceId); + } + + // the service has been injected in some special way, just return the service + return $this->containerBuilder->get($serviceId); + } + + /** + * Filters the given array of service ids by the given string filter: + * + * * An exact filter, "foo", will return *only* the "foo" service + * * A wildcard filter, "foo*", will return all services matching the wildcard + * + * @param array $serviceIds The array of service ids + * @param string $filter The given filter. If ending in *, a wildcard + * @return array + */ + private function filterServices($serviceIds, $filter, $onlyPublic = true) + { + // alphabetical so that this reads like an index of services + asort($serviceIds); + + if (!$filter) { + return $serviceIds; + } + + $regex = $this->buildFilterRegularExpression($filter); + $filteredIds = array(); + foreach ($serviceIds as $serviceId) { + if (preg_match($regex, $serviceId)) { + $filteredIds[] = $serviceId; + } + } + + if (!$filteredIds) { + // give a different message if the use was searching for an exact service + if (false === strpos($filter, '*')) { + $message = sprintf('The service "%s" does not exist.', $filter); + } else { + $message = sprintf('No services matched the pattern "%s"', $filter); + } + + throw new \InvalidArgumentException($message); + } + + return $filteredIds; + } + + /** + * Given a string with wildcards denoted as asterisks (*), this returns + * the regular expression that can be used to match on the string. + * + * For example, *foo* would equate to: + * + * /^(.+?)*foo(.+?)*$/ + * + * @param string $filter The raw filter + * @return string The regular expression + */ + private function buildFilterRegularExpression($filter) + { + $regex = preg_quote(str_replace('*', '', $filter)); + + // process the "front" wildcard + if ('*' === substr($filter, 0, 1)) { + $regex = '(.+?)*'.$regex; + } + + // process the "end" wildcard + if ('*' === substr($filter, -1, 1)) { + $regex .= '(.+?)*'; + } + $regex = sprintf('/^%s$/', $regex); + + return $regex; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index 1c1be8275d..04a0f4a461 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -19,7 +19,7 @@ use Symfony\Component\Console\Output\Output; use Symfony\Component\Routing\Matcher\Dumper\ApacheMatcherDumper; /** - * RouterDebugCommand. + * A console command for retrieving information about routes * * @author Fabien Potencier */ diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php new file mode 100644 index 0000000000..d9239b2269 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Dumps the ContainerBuilder to a cache file so that it can be used by + * debugging tools such as the container:debug console command. + * + * @author Ryan Weaver + */ +class ContainerBuilderDebugDumpPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $file = self::getBuilderCacheFilename($container); + + if (false !== @file_put_contents($file, serialize($container))) { + chmod($file, 0666); + } else { + throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file)); + } + } + + /** + * Calculates the cache filename to be used to cache the ContainerBuilder + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * @return string + */ + public static function getBuilderCacheFilename(ContainerInterface $container) + { + $cacheDir = $container->getParameter('kernel.cache_dir'); + $name = $container->getParameter('kernel.name'); + $env = ucfirst($container->getParameter('kernel.environment')); + $debug = ($container->getParameter('kernel.debug')) ? 'Debug' : ''; + + return $cacheDir.'/'.$name.$env.$debug.'ProjectContainerBuilder.cache'; + } +} \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index e184f273d4..f38f114cbf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -21,6 +21,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddClassesToCach use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddClassesToAutoloadMapPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheWarmerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Scope; @@ -80,5 +81,6 @@ class FrameworkBundle extends Bundle $container->addCompilerPass(new AddClassesToAutoloadMapPass()); $container->addCompilerPass(new TranslatorPass()); $container->addCompilerPass(new AddCacheWarmerPass()); + $container->adDcompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_BEFORE_REMOVING); } }