Commit 7052d972 authored by Fabien Potencier's avatar Fabien Potencier

Merge branch '1.3'

* 1.3: (27 commits)
  updated docs
  deprecated the user() trait
  made it easier to get the user from the app
  updated the changelog
  Remove incorrect examples and fix grammar
  updated changelog and docs
  Fixed whitespace issue
  Add options request capability to controller collection
  Update Application add options method for handling option routing
  Basic documentation
  Use PHPUnit annotation to skip test on <5.4
  Document as mixed, may be a string intended to be resolved from the CallbackResolver
  Revert the CS fix and make a valid docComment
  More CS fixes
  CS Fixes
  Allow view listeners to be "skipped" by returning null
  Restricted visibility
  Fix docblock type
  Remove callable hint for 5.3 tests
  Avoid mutating state
  ...

Conflicts:
	src/Silex/Application.php
	src/Silex/Provider/SecurityServiceProvider.php
	tests/Silex/Tests/ApplicationTest.php
parents bdbf5b78 6a092186
......@@ -21,6 +21,9 @@ Changelog
1.3.0 (2015-XX-XX)
------------------
* added a `$app['user']` to get the current user (security provider)
* added view handlers
* added support for the OPTIONS HTTP method
* added caching for the Translator provider
* deprecated `$app['exception_handler']->disable()` in favor of `unset($app['exception_handler'])`
* made Silex compatible with Symfony 2.7 (and keep compatibility with Symfony 2.3, 2.5, and 2.6)
......
......@@ -100,7 +100,7 @@ and values are options::
'dbname' => 'my_database',
'user' => 'my_username',
'password' => 'my_password',
'charset' => 'utf8',
'charset' => 'utf8mb4',
),
'mysql_write' => array(
'driver' => 'pdo_mysql',
......@@ -108,7 +108,7 @@ and values are options::
'dbname' => 'my_database',
'user' => 'my_username',
'password' => 'my_password',
'charset' => 'utf8',
'charset' => 'utf8mb4',
),
),
));
......
......@@ -43,6 +43,8 @@ Services
* **security.encoder.digest**: The encoder to use by default for all users.
* **user**: Returns the current user
.. note::
The service provider defines many other services that are used internally
......@@ -639,8 +641,6 @@ Traits
``Silex\Application\SecurityTrait`` adds the following shortcuts:
* **user**: Returns the current user.
* **encodePassword**: Encode a given password.
.. code-block:: php
......
......@@ -75,8 +75,9 @@ route is matched. A route pattern consists of:
pattern can include variable parts and you are able to set RegExp
requirements for them.
* *Method*: One of the following HTTP methods: ``GET``, ``POST``, ``PUT`` or
``DELETE`` or ``PATCH``. This describes the interaction with the resource.
* *Method*: One of the following HTTP methods: ``GET``, ``POST``, ``PUT``,
``DELETE``, ``PATCH``, or ``OPTIONS``. This describes the interaction with
the resource.
The controller is defined using a closure like this::
......@@ -550,6 +551,48 @@ early::
return new Response(...);
});
View Handlers
-------------
View Handlers allow you to intercept a controller result that is not a
``Response`` and transform it before it gets returned to the kernel.
To register a view handler, pass a callable (or string that can be resolved to a
callable) to the view method. The callable should accept some sort of result
from the controller::
$app->view(function (array $controllerResult) use ($app) {
return $app->json($controllerResult);
});
View Handlers also receive the ``Request`` as their second argument,
making them a good candidate for basic content negotiation::
$app->view(function (array $controllerResult, Request $request) use ($app) {
$acceptHeader = $request->headers->get('Accept');
$bestFormat = $app['negotiator']->getBestFormat($acceptHeader, array('json', 'xml'));
if ('json' === $bestFormat) {
return new JsonResponse($controllerResult);
}
if ('xml' === $bestFormat) {
return $app['serializer.xml']->renderResponse($controllerResult);
}
return $controllerResult;
});
View Handlers will be examined in the order they are added to the application
and Silex will use type hints to determine if a view handler should be used for
the current result, continuously using the return value of the last view handler
as the input for the next.
.. note::
You must ensure that Silex receives a ``Response`` or a string as the result of
the last view handler (or controller) to be run.
Redirects
---------
......
......@@ -224,6 +224,19 @@ class Application extends Container implements HttpKernelInterface, TerminableIn
return $this['controllers']->delete($pattern, $to);
}
/**
* Maps an OPTIONS request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
*
* @return Controller
*/
public function options($pattern, $to = null)
{
return $this['controllers']->options($pattern, $to);
}
/**
* Maps a PATCH request to a callable.
*
......@@ -365,6 +378,23 @@ class Application extends Container implements HttpKernelInterface, TerminableIn
$this->on(KernelEvents::EXCEPTION, new ExceptionListenerWrapper($this, $callback), $priority);
}
/**
* Registers a view handler.
*
* View handlers are simple callables which take a controller result and the
* request as arguments, whenever a controller returns a value that is not
* an instance of Response. When this occurs, all suitable handlers will be
* called, until one returns a Response object.
*
* @param mixed $callback View handler callback
* @param int $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0)
*/
public function view($callback, $priority = 0)
{
$this->on(KernelEvents::VIEW, new ViewListenerWrapper($this, $callback), $priority);
}
/**
* Flushes the controller collection.
*
......
......@@ -22,26 +22,6 @@ use Symfony\Component\Security\Core\User\UserInterface;
*/
trait SecurityTrait
{
/**
* Gets a user from the Security Context.
*
* @return mixed
*
* @see TokenInterface::getUser()
*/
public function user()
{
if (null === $token = $this['security.token_storage']->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
return;
}
return $user;
}
/**
* Encodes the raw password.
*
......
......@@ -136,6 +136,19 @@ class ControllerCollection
return $this->match($pattern, $to)->method('DELETE');
}
/**
* Maps an OPTIONS request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
*
* @return Controller
*/
public function options($pattern, $to = null)
{
return $this->match($pattern, $to)->method('OPTIONS');
}
/**
* Maps a PATCH request to a callable.
*
......
......@@ -105,6 +105,18 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
};
}
$app['user'] = $app->factory(function ($app) {
if (null === $token = $app['security.token_storage']->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
return;
}
return $user;
});
$app['security.authentication_manager'] = function ($app) {
$manager = new AuthenticationProviderManager($app['security.authentication_providers']);
$manager->setEventDispatcher($app['dispatcher']);
......
<?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\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
/**
* Wraps view listeners.
*
* @author Dave Marshall <dave@atst.io>
*/
class ViewListenerWrapper
{
private $app;
private $callback;
/**
* Constructor.
*
* @param Application $app An Application instance
* @param mixed $callback
*/
public function __construct(Application $app, $callback)
{
$this->app = $app;
$this->callback = $callback;
}
public function __invoke(GetResponseForControllerResultEvent $event)
{
$controllerResult = $event->getControllerResult();
$callback = $this->app['callback_resolver']->resolveCallback($this->callback);
if (!$this->shouldRun($callback, $controllerResult)) {
return;
}
$response = call_user_func($callback, $controllerResult, $event->getRequest());
if ($response instanceof Response) {
$event->setResponse($response);
} elseif (null !== $response) {
$event->setControllerResult($response);
}
}
private function shouldRun($callback, $controllerResult)
{
if (is_array($callback)) {
$callbackReflection = new \ReflectionMethod($callback[0], $callback[1]);
} elseif (is_object($callback) && !$callback instanceof \Closure) {
$callbackReflection = new \ReflectionObject($callback);
$callbackReflection = $callbackReflection->getMethod('__invoke');
} else {
$callbackReflection = new \ReflectionFunction($callback);
}
if ($callbackReflection->getNumberOfParameters() > 0) {
$parameters = $callbackReflection->getParameters();
$expectedControllerResult = $parameters[0];
if ($expectedControllerResult->getClass() && (!is_object($controllerResult) || !$expectedControllerResult->getClass()->isInstance($controllerResult))) {
return false;
}
if ($expectedControllerResult->isArray() && !is_array($controllerResult)) {
return false;
}
if (method_exists($expectedControllerResult, 'isCallable') && $expectedControllerResult->isCallable() && !is_callable($controllerResult)) {
return false;
}
}
return true;
}
}
......@@ -13,7 +13,6 @@ namespace Silex\Tests\Application;
use Silex\Provider\SecurityServiceProvider;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\HttpFoundation\Request;
/**
......@@ -23,47 +22,6 @@ use Symfony\Component\HttpFoundation\Request;
*/
class SecurityTraitTest extends \PHPUnit_Framework_TestCase
{
public function testUser()
{
$request = Request::create('/');
$app = $this->createApplication(array(
'fabien' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
));
$app->get('/', function () { return 'foo'; });
$app->handle($request);
$this->assertNull($app->user());
$request->headers->set('PHP_AUTH_USER', 'fabien');
$request->headers->set('PHP_AUTH_PW', 'foo');
$app->handle($request);
$this->assertInstanceOf('Symfony\Component\Security\Core\User\UserInterface', $app->user());
$this->assertEquals('fabien', $app->user()->getUsername());
}
public function testUserWithNoToken()
{
$request = Request::create('/');
$app = $this->createApplication();
$app->get('/', function () { return 'foo'; });
$app->handle($request);
$this->assertNull($app->user());
}
public function testUserWithInvalidUser()
{
$request = Request::create('/');
$app = $this->createApplication();
$app->boot();
$app['security.token_storage']->setToken(new UsernamePasswordToken('foo', 'foo', 'foo'));
$app->get('/', function () { return 'foo'; });
$app->handle($request);
$this->assertNull($app->user());
}
public function testEncodePassword()
{
$app = $this->createApplication(array(
......
......@@ -526,6 +526,115 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$response = $app->handle(Request::create('/'));
$this->assertEquals('ok', $response->getContent());
}
public function testViewListenerWithPrimitive()
{
$app = new Application();
$app->get('/foo', function () { return 123; });
$app->view(function ($view, Request $request) {
return new Response($view);
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('123', $response->getContent());
}
public function testViewListenerWithArrayTypeHint()
{
$app = new Application();
$app->get('/foo', function () { return array('ok'); });
$app->view(function (array $view) {
return new Response($view[0]);
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('ok', $response->getContent());
}
public function testViewListenerWithObjectTypeHint()
{
$app = new Application();
$app->get('/foo', function () { return (object) array('name' => 'world'); });
$app->view(function (\stdClass $view) {
return new Response('Hello '.$view->name);
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
/**
* @requires PHP 5.4
*/
public function testViewListenerWithCallableTypeHint()
{
$app = new Application();
$app->get('/foo', function () { return function () { return 'world'; }; });
$app->view(function (callable $view) {
return new Response('Hello '.$view());
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
public function testViewListenersCanBeChained()
{
$app = new Application();
$app->get('/foo', function () { return (object) array('name' => 'world'); });
$app->view(function (\stdClass $view) {
return array('msg' => 'Hello '.$view->name);
});
$app->view(function (array $view) {
return $view['msg'];
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
public function testViewListenersAreIgnoredIfNotSuitable()
{
$app = new Application();
$app->get('/foo', function () { return 'Hello world'; });
$app->view(function (\stdClass $view) {
throw new \Exception('View listener was called');
});
$app->view(function (array $view) {
throw new \Exception('View listener was called');
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
public function testViewListenersResponsesAreNotUsedIfNull()
{
$app = new Application();
$app->get('/foo', function () { return 'Hello world'; });
$app->view(function ($view) {
return 'Hello view listener';
});
$app->view(function ($view) {
return;
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello view listener', $response->getContent());
}
}
class FooController
......
......@@ -16,6 +16,7 @@ use Silex\WebTestCase;
use Silex\Provider\SecurityServiceProvider;
use Silex\Provider\SessionServiceProvider;
use Silex\Provider\ValidatorServiceProvider;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\HttpKernel\Client;
use Symfony\Component\HttpFoundation\Request;
......@@ -178,6 +179,70 @@ class SecurityServiceProviderTest extends WebTestCase
$this->assertCount(1, unserialize(serialize($app['routes'])));
}
public function testUser()
{
$app = new Application();
$app->register(new SecurityServiceProvider(), array(
'security.firewalls' => array(
'default' => array(
'http' => true,
'users' => array(
'fabien' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
),
),
),
));
$app->get('/', function () { return 'foo'; });
$request = Request::create('/');
$app->handle($request);
$this->assertNull($app['user']);
$request->headers->set('PHP_AUTH_USER', 'fabien');
$request->headers->set('PHP_AUTH_PW', 'foo');
$app->handle($request);
$this->assertInstanceOf('Symfony\Component\Security\Core\User\UserInterface', $app['user']);
$this->assertEquals('fabien', $app['user']->getUsername());
}
public function testUserWithNoToken()
{
$app = new Application();
$app->register(new SecurityServiceProvider(), array(
'security.firewalls' => array(
'default' => array(
'http' => true,
),
),
));
$request = Request::create('/');
$app->get('/', function () { return 'foo'; });
$app->handle($request);
$this->assertNull($app['user']);
}
public function testUserWithInvalidUser()
{
$app = new Application();
$app->register(new SecurityServiceProvider(), array(
'security.firewalls' => array(
'default' => array(
'http' => true,
),
),
));
$request = Request::create('/');
$app->boot();
$app['security.token_storage']->setToken(new UsernamePasswordToken('foo', 'foo', 'foo'));
$app->get('/', function () { return 'foo'; });
$app->handle($request);
$this->assertNull($app['user']);
}
public function createApplication($authenticationMethod = 'form')
{
$app = new Application();
......
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