Skip to content

Commit

Permalink
feature #24403 [FrameworkBundle][Routing] Show welcome message if no …
Browse files Browse the repository at this point in the history
…routes are configured (yceruto)

This PR was merged into the 3.4 branch.

Discussion
----------

[FrameworkBundle][Routing] Show welcome message if no routes are configured

| Q             | A
| ------------- | ---
| Branch?       | 3.4 (it would be good, else 4.1)
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | symfony/flex#20
| License       | MIT
| Doc PR        | -

Another attempt to improve the first time experience with a different technical approach this time.

Just after a fresh "SymfonyFlex" installation:
```bash
$ composer create-project symfony/skeleton flex
$ cd flex
$ make serve
```

![welcome](https://user-images.githubusercontent.com/2028198/31088339-4b84f95a-a76e-11e7-8b70-be53507f18e1.png)

When the first route is added, this message is no longer displayed (same if debug mode is disabled).

ping @javiereguiluz, @sstok

Commits
-------

e097ab3 Show welcome message if no routing configuration could be found
  • Loading branch information
fabpot committed Oct 2, 2017
2 parents 5e2f869 + e097ab3 commit 5f55bef
Show file tree
Hide file tree
Showing 14 changed files with 228 additions and 2 deletions.
Expand Up @@ -109,6 +109,8 @@
<argument type="service" id="request_stack" />
<argument type="service" id="router.request_context" on-invalid="ignore" />
<argument type="service" id="logger" on-invalid="ignore" />
<argument>%kernel.project_dir%</argument>
<argument>%kernel.debug%</argument>
</service>
</services>
</container>
Expand Up @@ -12,13 +12,16 @@
namespace Symfony\Component\HttpKernel\EventListener;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
Expand All @@ -31,23 +34,28 @@
* Initializes the context from the request and sets request attributes based on a matching route.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class RouterListener implements EventSubscriberInterface
{
private $matcher;
private $context;
private $logger;
private $requestStack;
private $projectDir;
private $debug;

/**
* @param UrlMatcherInterface|RequestMatcherInterface $matcher The Url or Request matcher
* @param RequestStack $requestStack A RequestStack instance
* @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface)
* @param LoggerInterface|null $logger The logger
* @param string $projectDir
* @param bool $debug
*
* @throws \InvalidArgumentException
*/
public function __construct($matcher, RequestStack $requestStack, RequestContext $context = null, LoggerInterface $logger = null)
public function __construct($matcher, RequestStack $requestStack, RequestContext $context = null, LoggerInterface $logger = null, $projectDir = null, $debug = true)
{
if (!$matcher instanceof UrlMatcherInterface && !$matcher instanceof RequestMatcherInterface) {
throw new \InvalidArgumentException('Matcher must either implement UrlMatcherInterface or RequestMatcherInterface.');
Expand All @@ -61,6 +69,8 @@ public function __construct($matcher, RequestStack $requestStack, RequestContext
$this->context = $context ?: $matcher->getContext();
$this->requestStack = $requestStack;
$this->logger = $logger;
$this->projectDir = $projectDir;
$this->debug = $debug;
}

private function setCurrentRequest(Request $request = null)
Expand Down Expand Up @@ -114,6 +124,12 @@ public function onKernelRequest(GetResponseEvent $event)
unset($parameters['_route'], $parameters['_controller']);
$request->attributes->set('_route_params', $parameters);
} catch (ResourceNotFoundException $e) {
if ($this->debug && $e instanceof NoConfigurationException) {
$event->setResponse($this->createWelcomeResponse());

return;
}

$message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo());

if ($referer = $request->headers->get('referer')) {
Expand All @@ -135,4 +151,16 @@ public static function getSubscribedEvents()
KernelEvents::FINISH_REQUEST => array(array('onKernelFinishRequest', 0)),
);
}

private function createWelcomeResponse()
{
$version = Kernel::VERSION;
$baseDir = realpath($this->projectDir).DIRECTORY_SEPARATOR;
$docVersion = substr(Kernel::VERSION, 0, 3);

ob_start();
include __DIR__.'/../Resources/welcome.html.php';

return new Response(ob_get_clean(), Response::HTTP_NOT_FOUND);
}
}
84 changes: 84 additions & 0 deletions src/Symfony/Component/HttpKernel/Resources/welcome.html.php
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Welcome!</title>
<style>
body { background: #F5F5F5; font: 18px/1.5 sans-serif; }
h1, h2 { line-height: 1.2; margin: 0 0 .5em; }
h1 { font-size: 36px; }
h2 { font-size: 21px; margin-bottom: 1em; }
p { margin: 0 0 1em 0; }
a { color: #0000F0; }
a:hover { text-decoration: none; }
code { background: #F5F5F5; max-width: 100px; padding: 2px 6px; word-wrap: break-word; }
#wrapper { background: #FFF; margin: 1em auto; max-width: 800px; width: 95%; }
#container { padding: 2em; }
#welcome, #status { margin-bottom: 2em; }
#welcome h1 span { display: block; font-size: 75%; }
#comment { font-size: 14px; text-align: center; color: #777777; background: #FEFFEA; padding: 10px; }
#comment p { margin-bottom: 0; }
#icon-status, #icon-book { float: left; height: 64px; margin-right: 1em; margin-top: -4px; width: 64px; }
#icon-book { display: none; }

@media (min-width: 768px) {
#wrapper { width: 80%; margin: 2em auto; }
#icon-book { display: inline-block; }
#status a, #next a { display: block; }

@-webkit-keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
.sf-toolbar { opacity: 0; -webkit-animation: fade-in 1s .2s forwards; animation: fade-in 1s .2s forwards;}
}
</style>
</head>
<body>
<div id="wrapper">
<div id="container">
<div id="welcome">
<h1><span>Welcome to</span> Symfony <?php echo $version; ?></h1>
</div>

<div id="status">
<p>
<svg id="icon-status" width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1671 566q0 40-28 68l-724 724-136 136q-28 28-68 28t-68-28l-136-136-362-362q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 295 656-657q28-28 68-28t68 28l136 136q28 28 28 68z" fill="#759E1A"/></svg>

Your application is now ready. You can start working on it at:<br>
<code><?php echo $baseDir; ?></code>
</p>
</div>

<div id="next">
<h2>What's next?</h2>
<p>
<svg id="icon-book" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="-12.5 9 64 64" enable-background="new -12.5 9 64 64" xml:space="preserve">
<path fill="#AAA" d="M6.8,40.8c2.4,0.8,4.5-0.7,4.9-2.5c0.2-1.2-0.3-2.1-1.3-3.2l-0.8-0.8c-0.4-0.5-0.6-1.3-0.2-1.9
c0.4-0.5,0.9-0.8,1.8-0.5c1.3,0.4,1.9,1.3,2.9,2.2c-0.4,1.4-0.7,2.9-0.9,4.2l-0.2,1c-0.7,4-1.3,6.2-2.7,7.5
c-0.3,0.3-0.7,0.5-1.3,0.6c-0.3,0-0.4-0.3-0.4-0.3c0-0.3,0.2-0.3,0.3-0.4c0.2-0.1,0.5-0.3,0.4-0.8c0-0.7-0.6-1.3-1.3-1.3
c-0.6,0-1.4,0.6-1.4,1.7s1,1.9,2.4,1.8c0.8,0,2.5-0.3,4.2-2.5c2-2.5,2.5-5.4,2.9-7.4l0.5-2.8c0.3,0,0.5,0.1,0.8,0.1
c2.4,0.1,3.7-1.3,3.7-2.3c0-0.6-0.3-1.2-0.9-1.2c-0.4,0-0.8,0.3-1,0.8c-0.1,0.6,0.8,1.1,0.1,1.5c-0.5,0.3-1.4,0.6-2.7,0.4l0.3-1.3
c0.5-2.6,1-5.7,3.2-5.8c0.2,0,0.8,0,0.8,0.4c0,0.2,0,0.2-0.2,0.5c-0.2,0.3-0.3,0.4-0.2,0.7c0,0.7,0.5,1.1,1.2,1.1
c0.9,0,1.2-1,1.2-1.4c0-1.2-1.2-1.8-2.6-1.8c-1.5,0.1-2.8,0.9-3.7,2.1c-1.1,1.3-1.8,2.9-2.3,4.5c-0.9-0.8-1.6-1.8-3.1-2.3
c-1.1-0.7-2.3-0.5-3.4,0.3c-0.5,0.4-0.8,1-1,1.6c-0.4,1.5,0.4,2.9,0.8,3.4l0.9,1c0.2,0.2,0.6,0.8,0.4,1.5c-0.3,0.8-1.2,1.3-2.1,1
c-0.4-0.2-1-0.5-0.9-0.9c0.1-0.2,0.2-0.3,0.3-0.5s0.1-0.3,0.1-0.3c0.2-0.6-0.1-1.4-0.7-1.6c-0.6-0.2-1.2,0-1.3,0.8
C4.3,38.4,4.7,40,6.8,40.8z M46.1,20.9c0-4.2-3.2-7.5-7.1-7.5h-3.8C34.8,10.8,32.7,9,30.2,9L-2.3,9.1c-2.8,0.1-4.9,2.4-4.9,5.4
L-7,58.6c0,4.8,8.1,13.9,11.6,14.1l34.7-0.1c3.9,0,7-3.4,7-7.6L46.1,20.9z M-0.3,36.4c0-8.6,6.5-15.6,14.5-15.6
c8,0,14.5,7,14.5,15.6S22.1,52,14.2,52C6.1,52-0.3,45-0.3,36.4z M42.1,65.1c0,1.8-1.5,3.1-3.1,3.1H4.6c-0.7,0-3-1.8-4.5-4.4h30.4
c2.8,0,5-2.4,5-5.4V17.9h3.7c1.6,0,2.9,1.4,2.9,3.1V65.1L42.1,65.1z"/>
</svg>

Read the documentation to learn
<a href="https://symfony.com/doc/<?php echo $docVersion; ?>/page_creation.html">
How to create your first page in Symfony
</a>
</p>
</div>
</div>
<div id="comment">
<p>
You're seeing this message because you have debug mode enabled and you haven't configured any URLs.
</p>
</div>
</div>
</body>
</html>
Expand Up @@ -24,6 +24,7 @@
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\RequestContext;

class RouterListenerTest extends TestCase
Expand Down Expand Up @@ -185,4 +186,26 @@ public function testWithBadRequest()
$response = $kernel->handle($request);
$this->assertSame(400, $response->getStatusCode());
}

public function testNoRoutingConfigurationResponse()
{
$requestStack = new RequestStack();

$requestMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\RequestMatcherInterface')->getMock();
$requestMatcher
->expects($this->once())
->method('matchRequest')
->willThrowException(new NoConfigurationException())
;

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($requestMatcher, $requestStack, new RequestContext()));

$kernel = new HttpKernel($dispatcher, new ControllerResolver(), $requestStack, new ArgumentResolver());

$request = Request::create('http://localhost/');
$response = $kernel->handle($request);
$this->assertSame(404, $response->getStatusCode());
$this->assertContains('Welcome', $response->getContent());
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/HttpKernel/composer.json
Expand Up @@ -33,7 +33,7 @@
"symfony/expression-language": "~2.8|~3.0|~4.0",
"symfony/finder": "~2.8|~3.0|~4.0",
"symfony/process": "~2.8|~3.0|~4.0",
"symfony/routing": "~2.8|~3.0|~4.0",
"symfony/routing": "~3.4|~4.0",
"symfony/stopwatch": "~2.8|~3.0|~4.0",
"symfony/templating": "~2.8|~3.0|~4.0",
"symfony/translation": "~2.8|~3.0|~4.0",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Routing/CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
3.4.0
-----

* Added `NoConfigurationException`.
* Added the possibility to define a prefix for all routes of a controller via @Route(name="prefix_")
* Added support for prioritized routing loaders.
* Add matched and default parameters to redirect responses
Expand Down
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Routing\Exception;

/**
* Exception thrown when no routes are configured.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class NoConfigurationException extends ResourceNotFoundException
{
}
Expand Up @@ -155,6 +155,12 @@ private function compileRoutes(RouteCollection $routes, $supportsRedirections)
}
}

if ('' === $code) {
$code .= " if ('/' === \$pathinfo) {\n";
$code .= " throw new Symfony\Component\Routing\Exception\NoConfigurationException();\n";
$code .= " }\n";
}

return $code;
}

Expand Down
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Routing\Matcher;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;

Expand All @@ -32,6 +33,7 @@ interface RequestMatcherInterface
*
* @return array An array of parameters
*
* @throws NoConfigurationException If no routing configuration could be found
* @throws ResourceNotFoundException If no matching resource could be found
* @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed
*/
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/Routing/Matcher/UrlMatcher.php
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Routing\Matcher;

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
Expand Down Expand Up @@ -91,6 +92,10 @@ public function match($pathinfo)
return $ret;
}

if (0 === count($this->routes) && '/' === $pathinfo) {
throw new NoConfigurationException();
}

throw 0 < count($this->allow)
? new MethodNotAllowedException(array_unique($this->allow))
: new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
Expand Down Expand Up @@ -123,6 +128,7 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
*
* @return array An array of parameters
*
* @throws NoConfigurationException If no routing configuration could be found
* @throws ResourceNotFoundException If the resource could not be found
* @throws MethodNotAllowedException If the resource was found but the request method is not allowed
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Routing/Matcher/UrlMatcherInterface.php
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Routing\Matcher;

use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\RequestContextAwareInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
Expand All @@ -32,6 +33,7 @@ interface UrlMatcherInterface extends RequestContextAwareInterface
*
* @return array An array of parameters
*
* @throws NoConfigurationException If no routing configuration could be found
* @throws ResourceNotFoundException If the resource could not be found
* @throws MethodNotAllowedException If the resource was found but the request method is not allowed
*/
Expand Down
@@ -0,0 +1,39 @@
<?php

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;

/**
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
public function __construct(RequestContext $context)
{
$this->context = $context;
}

public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();

if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}


if ('/' === $pathinfo) {
throw new Symfony\Component\Routing\Exception\NoConfigurationException();
}

throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}
Expand Up @@ -374,6 +374,7 @@ public function getRouteCollections()
$trailingSlashCollection->add('regex_not_trailing_slash_POST_method', new Route('/not-trailing/regex/post-method/{param}', array(), array(), array(), '', array(), array('POST')));

return array(
array(new RouteCollection(), 'url_matcher0.php', array()),
array($collection, 'url_matcher1.php', array()),
array($redirectCollection, 'url_matcher2.php', array('base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher')),
array($rootprefixCollection, 'url_matcher3.php', array()),
Expand Down
11 changes: 11 additions & 0 deletions src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
Expand Up @@ -427,4 +427,15 @@ public function testHostIsCaseInsensitive()
$matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com'));
$this->assertEquals(array('_route' => 'foo', 'locale' => 'en'), $matcher->match('/'));
}

/**
* @expectedException \Symfony\Component\Routing\Exception\NoConfigurationException
*/
public function testNoConfiguration()
{
$coll = new RouteCollection();

$matcher = new UrlMatcher($coll, new RequestContext());
$matcher->match('/');
}
}

0 comments on commit 5f55bef

Please sign in to comment.