3

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?

Yahya Uddin
  • 26,997
  • 35
  • 140
  • 231

1 Answers1

5

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.

lolmx
  • 521
  • 6
  • 18
  • Yes I have required it. But the issue is I don't even have a `bin/console` as I am not using the full Symfony framework. Just select components. – Yahya Uddin Aug 01 '16 at 17:36
  • 1
    @lolmx - Might want to research a bit on the difference between individual Symfony components (such as the Routing component) and the Symfony framework. The frameworks implements the routing debug command. No amount of installing of individual components is going to make the routing debug command appear. – Cerad Aug 01 '16 at 18:04
  • @Cerad so your saying the debug command is a part of the framework and thus this is not possible? – Yahya Uddin Aug 01 '16 at 18:28
  • 1
    Yep. I suppose you could look at the source and try to get it to work but you might be better off just writing something yourself. – Cerad Aug 01 '16 at 19:53
  • 1
    The source code is here: https://github.com/symfony/framework-bundle/blob/master/Command/RouterDebugCommand.php – Pipe Aug 01 '16 at 20:38
  • @Cerad We need to require the right component which is implementing the routerDebugCommand, else if you're right it won't appear by magic. Thanks @Pipe for your link, I think requiring `symfony/framework-bundle`will do the trick. (Looking at dependencies of the command I guess you'll still need to require `symfony/console`) – lolmx Aug 02 '16 at 08:33
  • 1
    @lolmx - Sorry but not even close. You basically need not only the framework dependencies but all the framework wiring and what not. In other words, the actual framework. Simply bringing down the console component does not give you a working console. Down voting as your answer this does not address the question which is to avoid the framework. – Cerad Aug 02 '16 at 11:38
  • Answer updated. @YahyaUddin, hope this will help you. And I hope Cerad will retire his down vote :D Let me know guys – lolmx Aug 02 '16 at 16:08
  • Wow. After all that effort not even Donald Trump would down vote this. – Cerad Aug 03 '16 at 16:30