Commit c006b758 authored by Fabien Potencier's avatar Fabien Potencier

replaced the current way to create reusable applications

The main idea is the move of all controller-related code from Application to
ControllerCollection. Reusable applications are now instances of
ControllerCollection instead of Application instances. That way, there is only
one "true" Application instance, and thus only one Pimple container.

Reusable applications can now be packaged as classes, like Extensions. It
allows to package services and controllers into one extension.

Benefits:

 * less hackish (proper wrapping in a class, no need to add a temporary route, no need for the LazyApplication anymore)
 * less code
 * simple and straightforward code (no magic anymore)
 * less side-effects
 * fix most severe/annoying limitations of the current implementation (from what I've read in the mailing-list and the Github issues)
 * better encapsulation of "reusable" applications
 * better separation of concerns
 * simple usage is exactly the same as before (as Application proxies the ControllerCollection methods for the "default" app)

Upgrading is as simple as replacing Application with ControllerCollection for
reusable apps (note that a reusable app is not "standalone" anymore):

    $mounted = new ControllerCollection();
    $mounted->get('/bar', function() { return 'foo'; });

    $app->mount('/foo', $mounted);

A better way now is to create a class:

    use Silex\ApplicationExtensionInterface;

    class FooApplication implements ApplicationExtensionInterface
    {
        public function connect(Application $app)
        {
            $controllers = new ControllerCollection();
            $controllers->get('/bar', function() { return 'foo'; });

            return $controllers;
        }
    }

    $app->mount('/foo', new FooApplication());

Note that you get the "master" application, so that you have access to the
services defined here.

If you want to register services, you can do so in the same extension:

    use Silex\ApplicationExtensionInterface;
    use Silex\ExtensionInterface;

    class FooApplication implements ApplicationExtensionInterface, ExtensionInterface
    {
        public function register(Application $app)
        {
            $app['some_service'] = ...;
        }

        public function connect(Application $app)
        {
            $controllers = new ControllerCollection();
            $controllers->get('/bar', function(Application $app) {
                $app['some_service']->...;

                return 'foo';
            });

            return $controllers;
        }
    }

    $ext = new FooApplication();
    $app->register($ext);
    $app->mount('/foo', $ext);
parent 24e01cf2
This changelog references all backward incompatibilities as we introduce them:
* 2011-08-26: The way reusable applications work has changed. The `mount()`
method now takes an instance of `ControllerCollection` instead of an
`Application` one.
Before:
$app = new Application();
$app->get('/bar', function() { return 'foo'; });
return $app;
After:
$app = new ControllerCollection();
$app->get('/bar', function() { return 'foo'; });
return $app;
* 2011-08-08: The controller method configuration is now done on the Controller itself
Before:
......
Extensions
==========
Silex provides a common interface for extensions. These
define services on the application.
Extensions allow the developer to reuse parts of an application into another
one. Silex provides two interfaces for extensions: `ExtensionInterface` for
services and `ControllersExtensionInterface` for controllers.
Loading extensions
Service Extensions
------------------
In order to load and use an extension, you must register it
on the application::
Loading extensions
~~~~~~~~~~~~~~~~~~
In order to load and use a service extension, you must register it on the
application::
$app = new Silex\Application();
......@@ -24,7 +28,7 @@ will be set **before** the extension is registered::
));
Conventions
-----------
~~~~~~~~~~~
You need to watch out in what order you do certain things when
interacting with extensions. Just keep to these rules:
......@@ -51,7 +55,7 @@ Make sure to stick to this behavior when creating your
own extensions.
Included extensions
-------------------
~~~~~~~~~~~~~~~~~~~
There are a few extensions that you get out of the box.
All of these are within the ``Silex\Extension`` namespace.
......@@ -68,7 +72,7 @@ All of these are within the ``Silex\Extension`` namespace.
* :doc:`HttpCacheExtension <extensions/http_cache>`
Creating an extension
---------------------
~~~~~~~~~~~~~~~~~~~~~
Extensions must implement the ``Silex\ExtensionInterface``::
......@@ -168,3 +172,73 @@ option when registering the extension::
For libraries that do not use PHP 5.3 namespaces you can use ``registerPrefix``
instead of ``registerNamespace``, which will use an underscore as directory
delimiter.
Controllers Extensions
----------------------
Loading extensions
~~~~~~~~~~~~~~~~~~
In order to load and use a controller extension, you must "mount" its
controllers under a path::
$app = new Silex\Application();
$app->mount('/blog', new Acme\BlogExtension());
All controllers defined by the extension will now be available under the
`/blog` path.
Creating an extension
~~~~~~~~~~~~~~~~~~~~~
Extensions must implement the ``Silex\ControllersExtensionInterface``::
interface ControllersExtensionInterface
{
function connect(Application $app);
}
Here is an example of such an extension::
namespace Acme;
use Silex\Application;
use Silex\ControllersExtensionInterface;
class HelloExtension implements ControllersExtensionInterface
{
public function connect(Application $app)
{
$controllers = new ControllerCollection();
$controllers->get('/', function (Silex\Application $app) {
return $app->redirect('/hello');
});
return $controllers;
}
}
The ``connect`` method must return an instance of ``ControllerCollection``.
``ControllerCollection`` is the class where all controller related methods are
defined (like ``get``, ``post``, ``match``, ...).
.. tip::
The ``Application`` class acts in fact as a proxy for these methods.
You can now use this extension as follows::
$app = new Silex\Application();
$app->connect('/blog', new Acme\HelloExtension());
In this example, the ``/blog/`` path now references the controller defined in
the extension.
.. tip::
You can also define an extension that implements both the service and the
controller extension interface and package in the same class the services
needed to make your controllers work.
......@@ -489,48 +489,6 @@ correctly, to prevent Cross-Site-Scripting attacks.
);
});
Reusing applications
--------------------
To make your applications reusable, return the ``$app`` variable instead of
calling the ``run()`` method::
// blog.php
require_once __DIR__.'/silex.phar';
$app = new Silex\Application();
// define your blog app
$app->get('/post/{id}', function ($id) { ... });
// return the app instance
return $app;
Running this application can now be done like this::
$app = require __DIR__.'/blog.php';
$app->run();
This pattern allows you to easily "mount" this application under any other
one::
$blog = require __DIR__.'/blog.php';
$app = new Silex\Application();
$app->mount('/blog', $blog);
// define your main app
$app->run();
Now, blog posts are available under the ``/blog/post/{id}`` route, along side
any other routes you might have defined.
If you mount many applications, you might want to avoid the overhead of
loading them all on each request by using the ``LazyApplication`` wrapper::
$blog = new Silex\LazyApplication(__DIR__.'/blog.php');
Console
-------
......
......@@ -128,11 +128,7 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
*/
public function match($pattern, $to)
{
$route = new Route($pattern, array('_controller' => $to));
$controller = new Controller($route);
$this['controllers']->add($controller);
return $controller;
return $this['controllers']->match($pattern, $to);
}
/**
......@@ -145,7 +141,7 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
*/
public function get($pattern, $to)
{
return $this->match($pattern, $to)->method('GET');
return $this['controllers']->get($pattern, $to);
}
/**
......@@ -158,7 +154,7 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
*/
public function post($pattern, $to)
{
return $this->match($pattern, $to)->method('POST');
return $this['controllers']->post($pattern, $to);
}
/**
......@@ -171,7 +167,7 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
*/
public function put($pattern, $to)
{
return $this->match($pattern, $to)->method('PUT');
return $this['controllers']->put($pattern, $to);
}
/**
......@@ -184,7 +180,7 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
*/
public function delete($pattern, $to)
{
return $this->match($pattern, $to)->method('DELETE');
return $this['controllers']->delete($pattern, $to);
}
/**
......@@ -302,26 +298,20 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
/**
* Mounts an application under the given route prefix.
*
* @param string $prefix The route prefix
* @param Application|\Closure $app An Application instance or a Closure that returns an Application instance
* @param string $prefix The route prefix
* @param ControllerCollection|ControllersExtensionInterface $app A ControllerCollection or an ControllersExtensionInterface instance
*/
public function mount($prefix, $app)
{
$prefix = rtrim($prefix, '/');
$mountHandler = function (Request $request, $prefix) use ($app) {
if (is_callable($app)) {
$app = $app();
}
$app->flush($prefix);
if ($app instanceof ControllersExtensionInterface) {
$app = $app->connect($this);
}
return $app->handle($request);
};
if (!$app instanceof ControllerCollection) {
throw new \LogicException('The "mount" method takes either a ControllerCollection or a ControllersExtensionInterface instance.');
}
$this
->match($prefix.'/{path}', $mountHandler)
->assert('path', '.*')
->value('prefix', $prefix);
$this['routes']->addCollection($app->flush(), $prefix);
}
/**
......
......@@ -12,20 +12,94 @@
namespace Silex;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Silex\Controller;
/**
* A collection of Silex controllers.
* Builds Silex controllers.
*
* It acts as a staging area for routes. You are able to set the route name
* until flush() is called, at which point all controllers are frozen and
* converted to a RouteCollection.
*
* @author Igor Wiedler <igor@wiedler.ch>
* @author Fabien Potencier <fabien@symfony.com>
*/
class ControllerCollection
{
private $controllers = array();
/**
* Maps a pattern to a callable.
*
* You can optionally specify HTTP methods that should be matched.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
*
* @return Silex\Controller
*/
public function match($pattern, $to)
{
$route = new Route($pattern, array('_controller' => $to));
$controller = new Controller($route);
$this->add($controller);
return $controller;
}
/**
* Maps a GET request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
*
* @return Silex\Controller
*/
public function get($pattern, $to)
{
return $this->match($pattern, $to)->method('GET');
}
/**
* Maps a POST request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
*
* @return Silex\Controller
*/
public function post($pattern, $to)
{
return $this->match($pattern, $to)->method('POST');
}
/**
* Maps a PUT request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
*
* @return Silex\Controller
*/
public function put($pattern, $to)
{
return $this->match($pattern, $to)->method('PUT');
}
/**
* Maps a DELETE request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
*
* @return Silex\Controller
*/
public function delete($pattern, $to)
{
return $this->match($pattern, $to)->method('DELETE');
}
/**
* Adds a controller to the staging area.
*
......
<?php
/*
* This file is part of the Silex framework.
*
* (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 Silex;
/**
* Interface for reusable controllers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ControllersExtensionInterface
{
/**
* Returns routes to connect to the given application.
*
* @param Application $app An Application instance
*
* @return ControllerCollection A ControllerCollection instance
*/
function connect(Application $app);
}
<?php
/*
* This file is part of the Silex framework.
*
* (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 Silex;
use Symfony\Component\HttpFoundation\Request;
/**
* A Lazy application wrapper.
*
* Acts as a closure, so it can be used as a lazy app
* factory for Silex\Application::mount().
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class LazyApplication
{
protected $appPath;
protected $app;
/**
* Constructor.
*
* The $appPath argument is the path to a Silex app file.
* This file must return a Silex application.
*
* @param string $appPath The absolute path to a Silex app file
*/
public function __construct($appPath)
{
$this->appPath = $appPath;
}
/**
* Returns the application.
*/
public function __invoke()
{
if (!$this->app) {
$this->app = require $this->appPath;
}
if (!$this->app instanceof Application) {
throw new \InvalidArgumentException('The provided path did not return a Silex\Application on inclusion.');
}
return $this->app;
}
}
......@@ -12,7 +12,7 @@
namespace Silex\Tests;
use Silex\Application;
use Silex\LazyApplication;
use Silex\ControllerCollection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
......@@ -45,7 +45,7 @@ class FunctionalTest extends \PHPUnit_Framework_TestCase
public function testMount()
{
$mounted = new Application();
$mounted = new ControllerCollection();
$mounted->get('/{name}', function ($name) { return new Response($name); });
$app = new Application();
......@@ -54,77 +54,4 @@ class FunctionalTest extends \PHPUnit_Framework_TestCase
$response = $app->handle(Request::create('/hello/Silex'));
$this->assertEquals('Silex', $response->getContent());
}
public function testLazyMount()
{
$i = 0;
$mountedFactory = function () use (&$i) {
$i++;
$mounted = new Application();
$mounted->get('/{name}', function ($name) {
return new Response($name);
});
return $mounted;
};
$app = new Application();
$app->mount('/hello', $mountedFactory);
$app->get('/main', function () {
return new Response('main app');
});
$response = $app->handle(Request::create('/main'));
$this->assertEquals('main app', $response->getContent());
$this->assertEquals(0, $i);
$response = $app->handle(Request::create('/hello/Silex'));
$this->assertEquals('Silex', $response->getContent());
$this->assertEquals(1, $i);
}
public function testLazyMountWithAnExternalFile()
{
$tmp = sys_get_temp_dir().'/SilexLazyApp.php';
file_put_contents($tmp, <<<'EOF'
<?php
$app = new Silex\Application();
$app->get('/{name}', function ($name) { return new Symfony\Component\HttpFoundation\Response($name); });
return $app;
EOF
);
$mounted = new LazyApplication($tmp);
$app = new Application();
$app->mount('/hello', $mounted);
$response = $app->handle(Request::create('/hello/Silex'));
$this->assertEquals('Silex', $response->getContent());
unlink($tmp);
}
public function testLazyMountWithAnInvalidExternalFile()
{
$tmp = sys_get_temp_dir().'/SilexInvalidLazyApp.php';
file_put_contents($tmp, '');
$mounted = new LazyApplication($tmp);
$app = new Application();
unset($app['exception_handler']);
$app->mount('/hello', $mounted);
try {
$app->handle(Request::create('/hello/Silex'));
$this->fail('Invalid LazyApplications should throw an exception.');
} catch (\InvalidArgumentException $e) {
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment