Reputation: 28841
I am using Symfony's 3.1 Routing Component as a standalone component.
I wish to debug the routes.
According to this: http://symfony.com/doc/current/routing/debug.html
this is done by running the following command:
php bin/console debug:router
Although this is trivial for a project running the full Symfony framework, how do I run it when using the router component as a standalone module?
Upvotes: 3
Views: 3000
Reputation: 521
I'd have posted a comment but not enough reputation..
Anyway, you should try to require the debug component in your project in order to use it:
$ composer require symfony/debug
Answer update
Okay, I've done some research and testing, and finally got the router debug command working. However, I'm still using two symfony components, console and config, but I'm sure with some further searches you can avoid the config one.
I've created a brand new project:
$ composer init
$ composer require symfony/routing
$ composer require symfony/console
$ composer require symfony/config
Don't forget to autoload your source code in the composer.json:
{
"name": "lolmx/test",
"require": {
"php": "^5.6",
"symfony/console": "^3.1",
"symfony/routing": "^3.1",
"symfony/config": "^3.1"
},
"autoload": {
"psr-0": {
"": "src/"
}
}
}
Then $ composer install
.
Create the console file in your project directory $ touch bin/console
, and write it:
<?php
// Include composer autoloader
require_once __DIR__."/../vendor/autoload.php";
// Use statements
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Console\Application;
use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Loader\PhpFileLoader;
use AppBundle\Command\MyRouterDebugCommand;
$context = new RequestContext();
$locator = new FileLocator(array(__DIR__)); // I needed symfony/config for this
$router = new Router(
new PhpFileLoader($locator), // And this class depends upon too
'../src/AppBundle/Routes.php',
array(),
$context
);
$app = new Application();
$app->add(new MyRouterDebugCommand(null, $router));
$app->run();
?>
I simply instantiate my router, give it to my command, and add the command to the console application.
Here what my Routes.php looks like:
// src/AppBundle/Routes.php
<?php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add('name', new Route("/myRoute", array(), array(), array(), "myHost", array('http', 'https'), array('GET', 'PUT')));
// more routes added here
return $collection;
Now let's write the command class itself:
<?php
namespace AppBundle\Command;
use AppBundle\Descriptor\DescriptorHelper;
use AppBundle\Descriptor\TextDescriptor;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Routing\Route;
class MyRouterDebugCommand extends Command
{
private $router;
public function __construct($name = null, Router $router)
{
parent::__construct($name);
$this->router = $router;
}
/**
* {@inheritdoc}
*/
public function isEnabled()
{
if (is_null($this->router)) {
return false;
}
if (!$this->router instanceof RouterInterface) {
return false;
}
return parent::isEnabled();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('debug:router')
->setDefinition(array(
new InputArgument('name', InputArgument::OPTIONAL, 'A route name'),
new InputOption('show-controllers', null, InputOption::VALUE_NONE, 'Show assigned controllers in overview'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'),
))
->setDescription('Displays current routes for an application')
->setHelp(<<<'EOF'
The <info>%command.name%</info> displays the configured routes:
<info>php %command.full_name%</info>
EOF
)
;
}
/**
* {@inheritdoc}
*
* @throws \InvalidArgumentException When route does not exist
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
$helper = new DescriptorHelper();
if ($name) {
$route = $this->router->getRouteCollection()->get($name);
if (!$route) {
throw new \InvalidArgumentException(sprintf('The route "%s" does not exist.', $name));
}
$this->convertController($route);
$helper->describe($io, $route, array(
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'name' => $name,
'output' => $io,
));
} else {
$routes = $this->router->getRouteCollection();
foreach ($routes as $route) {
$this->convertController($route);
}
$helper->describe($io, $routes, array(
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'show_controllers' => $input->getOption('show-controllers'),
'output' => $io,
));
}
}
private function convertController(Route $route)
{
$nameParser = new TextDescriptor();
if ($route->hasDefault('_controller')) {
try {
$route->setDefault('_controller', $nameParser->build($route->getDefault('_controller')));
} catch (\InvalidArgumentException $e) {
}
}
}
}
Imagine you're using the default descriptor helper use Symfony\Component\Console\Descriptor\DescriptorHelper
$ php bin/console debug:router
Will end with this wonderful error:
[Symfony\Component\Console\Exception\InvalidArgumentException]
Object of type "Symfony\Component\Routing\RouteCollection" is not describable.
Okay, so we need to create our custom DescriptorHelper. First implements the interface
<?php
namespace AppBundle\Descriptor;
use Symfony\Component\Console\Descriptor\DescriptorInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
abstract class Descriptor implements DescriptorInterface
{
/**
* @var OutputInterface
*/
protected $output;
/**
* {@inheritdoc}
*/
public function describe(OutputInterface $output, $object, array $options = array())
{
$this->output = $output;
switch (true) {
case $object instanceof RouteCollection:
$this->describeRouteCollection($object, $options);
break;
case $object instanceof Route:
$this->describeRoute($object, $options);
break;
case is_callable($object):
$this->describeCallable($object, $options);
break;
default:
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
}
}
/**
* Returns the output.
*
* @return OutputInterface The output
*/
protected function getOutput()
{
return $this->output;
}
/**
* Writes content to output.
*
* @param string $content
* @param bool $decorated
*/
protected function write($content, $decorated = false)
{
$this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
}
/**
* Describes an InputArgument instance.
*
* @param RouteCollection $routes
* @param array $options
*/
abstract protected function describeRouteCollection(RouteCollection $routes, array $options = array());
/**
* Describes an InputOption instance.
*
* @param Route $route
* @param array $options
*/
abstract protected function describeRoute(Route $route, array $options = array());
/**
* Describes a callable.
*
* @param callable $callable
* @param array $options
*/
abstract protected function describeCallable($callable, array $options = array());
/**
* Formats a value as string.
*
* @param mixed $value
*
* @return string
*/
protected function formatValue($value)
{
if (is_object($value)) {
return sprintf('object(%s)', get_class($value));
}
if (is_string($value)) {
return $value;
}
return preg_replace("/\n\s*/s", '', var_export($value, true));
}
/**
* Formats a parameter.
*
* @param mixed $value
*
* @return string
*/
protected function formatParameter($value)
{
if (is_bool($value) || is_array($value) || (null === $value)) {
$jsonString = json_encode($value);
if (preg_match('/^(.{60})./us', $jsonString, $matches)) {
return $matches[1].'...';
}
return $jsonString;
}
return (string) $value;
}
}
Then override the default DescriptorHelper to register our descriptor
<?php
namespace AppBundle\Descriptor;
use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper;
class DescriptorHelper extends BaseDescriptorHelper
{
/**
* Constructor.
*/
public function __construct()
{
$this
->register('txt', new TextDescriptor())
;
}
}
And finally, implements our descriptor
<?php
namespace AppBundle\Descriptor;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class TextDescriptor extends Descriptor
{
/**
* {@inheritdoc}
*/
protected function describeRouteCollection(RouteCollection $routes, array $options = array())
{
$showControllers = isset($options['show_controllers']) && $options['show_controllers'];
$tableHeaders = array('Name', 'Method', 'Scheme', 'Host', 'Path');
if ($showControllers) {
$tableHeaders[] = 'Controller';
}
$tableRows = array();
foreach ($routes->all() as $name => $route) {
$row = array(
$name,
$route->getMethods() ? implode('|', $route->getMethods()) : 'ANY',
$route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY',
'' !== $route->getHost() ? $route->getHost() : 'ANY',
$route->getPath(),
);
if ($showControllers) {
$controller = $route->getDefault('_controller');
if ($controller instanceof \Closure) {
$controller = 'Closure';
} elseif (is_object($controller)) {
$controller = get_class($controller);
}
$row[] = $controller;
}
$tableRows[] = $row;
}
if (isset($options['output'])) {
$options['output']->table($tableHeaders, $tableRows);
} else {
$table = new Table($this->getOutput());
$table->setHeaders($tableHeaders)->setRows($tableRows);
$table->render();
}
}
/**
* {@inheritdoc}
*/
protected function describeRoute(Route $route, array $options = array())
{
$tableHeaders = array('Property', 'Value');
$tableRows = array(
array('Route Name', isset($options['name']) ? $options['name'] : ''),
array('Path', $route->getPath()),
array('Path Regex', $route->compile()->getRegex()),
array('Host', ('' !== $route->getHost() ? $route->getHost() : 'ANY')),
array('Host Regex', ('' !== $route->getHost() ? $route->compile()->getHostRegex() : '')),
array('Scheme', ($route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY')),
array('Method', ($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY')),
array('Requirements', ($route->getRequirements() ? $this->formatRouterConfig($route->getRequirements()) : 'NO CUSTOM')),
array('Class', get_class($route)),
array('Defaults', $this->formatRouterConfig($route->getDefaults())),
array('Options', $this->formatRouterConfig($route->getOptions())),
);
$table = new Table($this->getOutput());
$table->setHeaders($tableHeaders)->setRows($tableRows);
$table->render();
}
/**
* {@inheritdoc}
*/
protected function describeCallable($callable, array $options = array())
{
$this->writeText($this->formatCallable($callable), $options);
}
/**
* @param array $config
*
* @return string
*/
private function formatRouterConfig(array $config)
{
if (empty($config)) {
return 'NONE';
}
ksort($config);
$configAsString = '';
foreach ($config as $key => $value) {
$configAsString .= sprintf("\n%s: %s", $key, $this->formatValue($value));
}
return trim($configAsString);
}
/**
* @param callable $callable
*
* @return string
*/
private function formatCallable($callable)
{
if (is_array($callable)) {
if (is_object($callable[0])) {
return sprintf('%s::%s()', get_class($callable[0]), $callable[1]);
}
return sprintf('%s::%s()', $callable[0], $callable[1]);
}
if (is_string($callable)) {
return sprintf('%s()', $callable);
}
if ($callable instanceof \Closure) {
return '\Closure()';
}
if (method_exists($callable, '__invoke')) {
return sprintf('%s::__invoke()', get_class($callable));
}
throw new \InvalidArgumentException('Callable is not describable.');
}
/**
* @param string $content
* @param array $options
*/
private function writeText($content, array $options = array())
{
$this->write(
isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
isset($options['raw_output']) ? !$options['raw_output'] : true
);
}
}
Now writing $ php bin/console debug:router
will output
------ --------- ------------ -------- ----------
Name Method Scheme Host Path
------ --------- ------------ -------- ----------
name GET|PUT http|https myHost /myRoute
------ --------- ------------ -------- ----------
I dived into symfony source code to find all of this, and the different files are/may be snippets of code from Symfony, symfony routing, console and framework-bundle.
Upvotes: 5