Silex
Silex
The Silex Book This work is licensed under the Attribution-Share Alike 3.0 Unported license (http://creativecommons.org/ licenses/by-sa/3.0/). You are free to share (to copy, distribute and transmit the work), and to remix (to adapt the work) under the following conditions: Attribution: You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work). Share Alike: If you alter, transform, or build upon this work, you may distribute the resulting work only under the same, similar or a compatible license. For any reuse or distribution, you must make clear to others the license terms of this work. The information in this book is distributed on an as is basis, without warranty. Although every precaution has been taken in the preparation of this work, neither the author(s) nor SensioLabs shall have any liability to any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by the information contained in this work.
Contents at a Glance
Introduction .......................................................................................................................................4 Usage .................................................................................................................................................6 Services.............................................................................................................................................22 Providers ..........................................................................................................................................27 Testing .............................................................................................................................................31 Accepting a JSON request body.........................................................................................................36 Translating Validation Messages........................................................................................................39 How to use PdoSessionStorage to store sessions in the database .........................................................40 Disable CSRF Protection on a form using the FormExtension.............................................................42 How to use YAML to configure validation .........................................................................................43 Internals ...........................................................................................................................................44 Contributing.....................................................................................................................................46 DoctrineServiceProvider ....................................................................................................................47 MonologServiceProvider ...................................................................................................................50 SessionServiceProvider ......................................................................................................................53 SwiftmailerServiceProvider ................................................................................................................55 TranslationServiceProvider................................................................................................................58 TwigServiceProvider..........................................................................................................................62 UrlGeneratorServiceProvider .............................................................................................................65 ValidatorServiceProvider ...................................................................................................................67 FormServiceProvider .........................................................................................................................72 HttpCacheServiceProvider.................................................................................................................76 SecurityServiceProvider .....................................................................................................................79 Webserver Configuration ..................................................................................................................90 Changelog ........................................................................................................................................93 Phar File ...........................................................................................................................................96
Chapter 1
Introduction
Silex is a PHP microframework for PHP 5.3. It is built on the shoulders of Symfony2 and Pimple and also inspired by sinatra. A microframework provides the guts for building simple single-file apps. Silex aims to be: Concise: Silex exposes an intuitive and concise API that is fun to use. Extensible: Silex has an extension system based around the Pimple micro service-container that makes it even easier to tie in third party libraries. Testable: Silex uses Symfony2's HttpKernel which abstracts request and response. This makes it very easy to test apps and the framework itself. It also respects the HTTP specification and encourages its proper use. In a nutshell, you define controllers and map them to routes, all in one step. Let's go!:
Listing Listing 1 1-1 1-2
// web/index.php
require_once __DIR__.'/../vendor/autoload.php'; $app = new Silex\Application(); $app->get('/hello/{name}', function ($name) use ($app) { return 'Hello '.$app->escape($name); }); $app->run();
2 3 4 5 6 7 8 9 10 11
All that is needed to get access to the Framework is to include the autoloader. Next we define a route to /hello/{name} that matches for GET requests. When the route matches, the function is executed and the return value is sent back to the client. Finally, the app is run. Visit /hello/world to see the result. It's really that easy! Installing Silex is as easy as it can get. Download1 the archive file, extract it, and you're done!
Chapter 1: Introduction | 4
1. http://silex.sensiolabs.org/download
Chapter 1: Introduction | 5
Chapter 2
Usage
This chapter describes how to use Silex.
Installation
If you want to get started fast, download1 Silex as an archive and extract it, you should have the following directory structure:
Listing Listing 1 2-1 2-2
2 3 4 5 6
{ 2 3 4 5 6 }
1. http://silex.sensiolabs.org/download
Chapter 2: Usage | 6
Listing 2-6
Upgrading
Upgrading Silex to the latest version is as easy as running the update command:
Listing 2-8
Bootstrap
To bootstrap Silex, all you need to do is require the vendor/autoload.php file and create an instance of Silex\Application. After your controller definitions, call the run method on your application:
Listing // web/index.php 2-9
1 2 3 4 5 6 7 8 9
Listing 2-10
// definitions
$app->run();
Then, you have to configure your web server (read the dedicated chapter for more information).
When developing a website, you might want to turn on the debug mode to ease debugging:
Listing 1 $app['debug'] = true; 2-11
Listing 2-12
If your application is hosted behind a reverse proxy and you want Silex to trust the X-ForwardedFor* headers, you will need to run your application like this:
Listing 2-14
Chapter 2: Usage | 7
Routing
In Silex you define a route and the controller that is called when that route is matched. A route pattern consists of: Pattern: The route pattern defines a path that points to a resource. The 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 DELETE. This describes the interaction with the resource. Commonly only GET and POST are used, but it is possible to use the others as well. The controller is defined using a closure like this:
Listing Listing 1 2-15 2-16
function () { 2 // do something 3 }
Closures are anonymous functions that may import state from outside of their definition. This is different from globals, because the outer state does not have to be global. For instance, you could define a closure in a function and import local variables of that function.
Closures that do not import scope are referred to as lambdas. Because in PHP all anonymous functions are instances of the Closure class, we will not make a distinction here.
The return value of the closure becomes the content of the page. There is also an alternate way for defining controllers using a class method. The syntax for that is ClassName::methodName. Static methods are also possible.
$blogPosts = array( 2 1 => array( 3 'date' => '2011-03-29', 4 'author' => 'igorw', 5 'title' => 'Using Silex', 6 'body' => '...', 7 ), 8 ); 9 10 $app->get('/blog', function () use ($blogPosts) { 11 $output = ''; 12 foreach ($blogPosts as $post) { 13 $output .= $post['title']; 14 $output .= '<br />'; 15 } 16 17 return $output; 18 });
Chapter 2: Usage | 8
Visiting /blog will return a list of blog post titles. The use statement means something different in this context. It tells the closure to import the $blogPosts variable from the outer scope. This allows you to use it from within the closure.
Dynamic routing
Now, you can create another controller for viewing individual blog posts:
Listing 1 $app->get('/blog/show/{id}', function (Silex\Application $app, $id) use ($blogPosts) { 2-19 2 if (!isset($blogPosts[$id])) { 3 $app->abort(404, "Post $id does not exist."); 4 } 5 6 $post = $blogPosts[$id]; 7 8 return "<h1>{$post['title']}</h1>". 9 "<p>{$post['body']}</p>"; 10 });
Listing 2-20
This route definition has a variable {id} part which is passed to the closure. When the post does not exist, we are using abort() to stop the request early. It actually throws an exception, which we will see how to handle later on.
1 2 3 4 5 6 7 8 9
Listing 2-22
$app->post('/feedback', function (Request $request) { $message = $request->get('message'); mail('[email protected]', '[YourSite] Feedback', $message); return new Response('Thank you for your feedback!', 201); });
It is pretty straightforward.
There is a SwiftmailerServiceProvider included that you can use instead of mail().
The current request is automatically injected by Silex to the Closure thanks to the type hinting. It is an instance of Request2, so you can fetch variables using the request get method. Instead of returning a string we are returning an instance of Response3. This allows setting an HTTP status code, in this case it is set to 201 Created.
2. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Request.html 3. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Response.html
Chapter 2: Usage | 9
Silex always uses a Response internally, it converts strings to responses with status code 200 Ok.
Other methods
You can create controllers for most HTTP methods. Just call one of these methods on your application: get, post, put, delete. You can also call match, which will match all methods:
You can then restrict the allowed methods via the method method:
You can match multiple methods with one controller using regex syntax:
Listing Listing 1 2-27 2-28
The order in which the routes are defined is significant. The first matching route will be used, so place more generic routes at the bottom.
Route variables
As it has been shown before you can define variable parts in a route like this:
Listing Listing 1 2-29 2-30
It is also possible to have more than one variable part, just make sure the closure arguments match the names of the variable parts:
Chapter 2: Usage | 10
Listing 2-32
While it's not suggested, you could also do this (note the switched arguments):
Listing 1 $app->get('/blog/show/{postId}/{commentId}', function ($commentId, $postId) { 2-33 2 ... 3 });
Listing 2-34
You can also ask for the current Request and Application objects:
Listing 1 $app->get('/blog/show/{id}', function (Application $app, Request $request, $id) { 2-35 2 ... 3 });
Listing 2-36
Note for the Application and Request objects, Silex does the injection based on the type hinting and not on the variable name:
Listing 1 $app->get('/blog/show/{id}', function (Application $foo, Request $bar, $id) { 2 2-37 ... 3 });
Listing 2-38
Listing 2-40
This is useful when you want to convert route variables to objects as it allows to reuse the conversion code across different controllers:
1 2 3 4 5 6 7 8
Listing $userProvider = function ($id) { 2-41 return new User($id); };
Listing 2-42
Chapter 2: Usage | 11
The converter callback also receives the Request as its second argument:
$callback = function ($post, Request $request) { 2 return new Post($request->attributes->get('slug')); 3 }; 4 5 $app->get('/blog/{id}/{slug}', function (Post $post) { 6 // ... 7 })->convert('post', $callback);
Requirements
In some cases you may want to only match certain expressions. You can define requirements using regular expressions by calling assert on the Controller object, which is returned by the routing methods. The following will make sure the id argument is numeric, since \d+ matches any amount of digits:
Default values
You can define a default value for any route variable by calling value on the Controller object:
This will allow matching /, in which case the pageName variable will have the value index.
Chapter 2: Usage | 12
Named routes
Some providers (such as UrlGeneratorProvider) can make use of named routes. By default Silex will generate a route name for you, that cannot really be used. You can give a route a name by calling bind on the Controller object that is returned by the routing methods:
1 2 3 4 5 6 7 8 9
Listing 2-52
It only makes sense to name routes if you use providers that make use of the RouteCollection.
1 2 3 4 5 6 7 8 9 10 11
Listing 2-54
$app->after(function () { // tear down }); $app->finish(function () { // after response has been sent });
The before filter has access to the current Request, and can short-circuit the whole rendering by returning a Response:
Listing 1 $app->before(function (Request $request) { 2-55 2 // redirect the user to the login screen if access to the Resource is protected 3 if (...) { 4 return new RedirectResponse('/login'); 5 } 6 });
Listing 2-56
The after filter has access to the Request and the Response:
Chapter 2: Usage | 13
The finish filter has access to the Request and the Response:
Listing Listing 1 2-59 2-60
Route middlewares
Route middlewares are PHP callables which are triggered when their associated route is matched: before middlewares are fired just before the route callback, but after the application before filters; after middlewares are fired just after the route callback, but before the application after filters. This can be used for a lot of use cases; for instance, here is a simple "anonymous/logged user" check:
Listing Listing 1 2-61 2-62
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
$mustBeAnonymous = function (Request $request) use ($app) { if ($app['session']->has('userId')) { return $app->redirect('/user/logout'); } }; $mustBeLogged = function (Request $request) use ($app) { if (!$app['session']->has('userId')) { return $app->redirect('/user/login'); } }; $app->get('/user/subscribe', function () { ... }) ->before($mustBeAnonymous); $app->get('/user/login', function () { ... }) ->before($mustBeAnonymous); $app->get('/user/my-profile', function () { ...
Chapter 2: Usage | 14
25 }) 26 ->before($mustBeLogged);
The before and after methods can be called several times for a given route, in which case they are triggered in the same order as you added them to the route. For convenience, the before middlewares are called with the current Request instance as an argument and the after middlewares are called with the current Request and Response instance as arguments. If any of the before middlewares returns a Symfony HTTP Response, it will short-circuit the whole rendering: the next middlewares won't be run, neither the route callback. You can also redirect to another page by returning a redirect response, which you can create by calling the Application redirect method.
If a before middleware does not return a Symfony HTTP Response or null, a RuntimeException is thrown.
Global Configuration
If a controller setting must be applied to all controllers (a converter, a middleware, a requirement, or a default value), you can configure it on $app['controllers'], which holds all application controllers:
Listing 1 $app['controllers'] 2-63 2 ->value('id', '1') 3 ->assert('id', '\d+') 4 ->requireHttps() 5 ->method('get') 6 ->convert('id', function () { // ... }) 7 ->before(function () { // ... }) 8 ;
Listing 2-64
These settings are applied to already registered controllers and they become the defaults for new controllers.
The global configuration does not apply to controller providers you might mount as they have their own global configuration (see the Modularity paragraph below).
Error handlers
If some part of your code throws an exception you will want to display some kind of error page to the user. This is what error handlers do. You can also use them to do additional things, such as logging. To register an error handler, pass a closure to the error method which takes an Exception argument and returns a response:
Chapter 2: Usage | 15
use Symfony\Component\HttpFoundation\Response; 2 3 $app->error(function (\Exception $e, $code) { 4 return new Response('We are sorry, but something went terribly wrong.'); 5 });
You can also check for specific errors by using the $code argument, and handle them differently:
use Symfony\Component\HttpFoundation\Response; 2 3 $app->error(function (\Exception $e, $code) { 4 switch ($code) { 5 case 404: 6 $message = 'The requested page could not be found.'; 7 break; 8 default: 9 $message = 'We are sorry, but something went terribly wrong.'; 10 } 11 12 return new Response($message); 13 });
As Silex ensures that the Response status code is set to the most appropriate one depending on the exception, setting the status on the response won't work. If you want to overwrite the status code (which you should not without a good reason), set the X-Status-Code header:
return new Response('Error', 404 /* ignored */, array('X-Status-Code' => 200));
You can restrict an error handler to only handle some Exception classes by setting a more specific type hint for the Closure argument:
Listing Listing 1 2-71 2-72
$app->error(function (\LogicException $e, $code) { 2 // this handler will only \LogicException exceptions 3 // and exceptions that extends \LogicException 4 });
If you want to set up logging you can use a separate error handler for that. Just make sure you register it before the response error handlers, because once a response is returned, the following handlers are ignored.
Silex ships with a provider for Monolog4 which handles logging of errors. Check out the Providers chapter for details.
4. https://github.com/Seldaek/monolog
Chapter 2: Usage | 16
Silex comes with a default error handler that displays a detailed error message with the stack trace when debug is true, and a simple error message otherwise. Error handlers registered via the error() method always take precedence but you can keep the nice error messages when debug is turned on like this:
Listing Symfony\Component\HttpFoundation\Response; 1 use 2 2-73 3 $app->error(function (\Exception $e, $code) use ($app) { 4 if ($app['debug']) { 5 return; 6 } 7 8 // logic to handle the error and return a Response 9 });
Listing 2-74
The error handlers are also called when you use abort to abort a request early:
Listing 1 $app->get('/blog/show/{id}', function (Silex\Application $app, $id) use ($blogPosts) { 2-75 2 if (!isset($blogPosts[$id])) { 3 $app->abort(404, "Post $id does not exist."); 4 } 5 6 return new Response(...); 7 });
Listing 2-76
Redirects
You can redirect to another page by returning a redirect response, which you can create by calling the redirect method:
Listing 2-78
Forwards
When you want to delegate the rendering to another controller, without a round-trip to the browser (as for a redirect), use an internal sub-request:
Listing Symfony\Component\HttpFoundation\Request; 1 use 2-79 2 use Symfony\Component\HttpKernel\HttpKernelInterface; 3
Listing 2-80
Chapter 2: Usage | 17
4 $app->get('/', function () use ($app) { 5 // redirect to /hello 6 $subRequest = Request::create('/hello', 'GET'); 7 8 return $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST); 9 });
If you are using UrlGeneratorProvider, you can also generate the URI:
Modularity
When your application starts to define too many controllers, you might want to group them logically:
Listing Listing 1 2-83 2-84
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// define controllers for a blog $blog = $app['controllers_factory']; $blog->get('/', function () { return 'Blog home page'; }); // ... // define controllers for a forum $forum = $app['controllers_factory']; $forum->get('/', function () { return 'Forum home page'; }); // define "global" controllers $app->get('/', function () { return 'Main home page'; });
$app->mount('/blog', $blog); $app->mount('/forum', $forum);
mount() prefixes all routes with the given prefix and merges them into the main Application. So, / will map to the main home page, /blog/ to the blog home page, and /forum/ to the forum home page.
Chapter 2: Usage | 18
When mounting a route collection under /blog, it is not possible to define a route for the /blog URL. The shortest possible URL is /blog/.
When calling get(), match(), or any other HTTP methods on the Application, you are in fact calling them on a default instance of ControllerCollection (stored in $app['controllers']).
Another benefit is the ability to apply settings on a set of controllers very easily. Building on the example from the middleware section, here is how you would secure all controllers for the backend collection:
Listing 1 $backend = $app['controllers_factory']; 2-85 2 3 // ensure that all controllers require logged-in users 4 $backend->before($mustBeLogged);
Listing 2-86
For a better readability, you can split each controller collection into a separate file:
1 2 3 4 5 6 7 8
Listing // blog.php 2-87 $blog = $app['controllers_factory']; $blog->get('/', function () { return 'Blog home page'; });
Listing 2-88
return $blog;
JSON
If you want to return JSON data, you can use the json helper method. Simply pass it your data, status code and headers, and it will create a JSON response for you:
Listing 1 $app->get('/users/{id}', function ($id) use ($app) { 2-89 2 $user = getUser($id); 3 4 if (!$user) { 5 $error = array('message' => 'The user was not found.'); 6 return $app->json($error, 404); 7 } 8 9 return $app->json($user); 10 });
Listing 2-90
Chapter 2: Usage | 19
Streaming
It's possible to create a streaming response, which is important in cases when you cannot buffer the data being sent:
Listing Listing 1 2-91 2-92
$app->get('/images/{file}', function ($file) use ($app) { 2 if (!file_exists(__DIR__.'/images/'.$file)) { 3 return $app->abort(404, 'The image was not found.'); 4 } 5 6 $stream = function () use ($file) { 7 readfile($file); 8 }; 9 10 return $app->stream($stream, 200, array('Content-Type' => 'image/png')); 11 });
If you need to send chunks, make sure you call ob_flush and flush after every chunk:
$stream = function () { 2 $fh = fopen('http://www.example.com/', 'rb'); 3 while (!feof($fh)) { 4 echo fread($fh, 1024); 5 ob_flush(); 6 flush(); 7 } 8 fclose($fh); 9 };
Traits
Silex comes with PHP traits that define shortcut methods.
You need to use PHP 5.4 or later to benefit from this feature.
Almost all built-in service providers have some corresponding PHP traits. To use them, define your own Application class and include the traits you want:
Listing Listing 1 2-95 2-96
use Silex\Application; 2 3 class MyApplication extends Application 4 { 5 use Application\TwigTrait; 6 use Application\SecurityTrait; 7 use Application\FormTrait; 8 use Application\UrlGeneratorTrait; 9 use Application\SwiftmailerTrait;
Chapter 2: Usage | 20
10 11 12 }
You can also define your own Route class and use some traits:
1 2 3 4 5 6
Listing Silex\Route; use 2-97
Listing 2-98
Listing 2-100
Read each provider chapter to learn more about the added methods.
Security
Make sure to protect your application against attacks.
Escaping
When outputting any user input (either route variables GET/POST variables obtained from the request), you will have to make sure to escape it correctly, to prevent Cross-Site-Scripting attacks. Escaping HTML: PHP provides the htmlspecialchars function for this. Silex provides a shortcut escape method:
Listing 1 $app->get('/name', function (Silex\Application $app) { 2-101 2 $name = $app['request']->get('name'); 3 return "You provided the name {$app->escape($name)}."; 4 });
Listing 2-102
If you use the Twig template engine you should use its escaping or even auto-escaping mechanisms. Escaping JSON: If you want to provide data in JSON format you should use the Silex json function:
Listing 1 $app->get('/name.json', function (Silex\Application $app) { 2-103 2 $name = $app['request']->get('name'); 3 return $app->json(array('name' => $name)); 4 });
Listing 2-104
Chapter 2: Usage | 21
Chapter 3
Services
Silex is not only a microframework. It is also a micro service container. It does this by extending Pimple1 which provides service goodness in just 44 NCLOC.
Dependency Injection
You can skip this if you already know what Dependency Injection is.
Dependency Injection is a design pattern where you pass dependencies to services instead of creating them from within the service or relying on globals. This generally leads to code that is decoupled, reusable, flexible and testable. Here is an example of a class that takes a User object and stores it as a file in JSON format:
class JsonUserPersister 2 { 3 private $basePath; 4 5 public function __construct($basePath) 6 { 7 $this->basePath = $basePath; 8 } 9 10 public function persist(User $user) 11 { 12 $data = $user->getAttributes(); 13 $json = json_encode($data); 14 $filename = $this->basePath.'/'.$user->id.'.json';
1. http://pimple.sensiolabs.org
Chapter 3: Services | 22
15 16 17 }
In this simple example the dependency is the basePath property. It is passed to the constructor. This means you can create several independent instances with different base paths. Of course dependencies do not have to be simple strings. More often they are in fact other services.
Container
A DIC or service container is responsible for creating and storing services. It can recursively create dependencies of the requested services and inject them. It does so lazily, which means a service is only created when you actually need it. Most containers are quite complex and are configured through XML or YAML files. Pimple is different.
Pimple
Pimple is probably the simplest service container out there. It makes strong use of closures and implements the ArrayAccess interface. We will start off by creating a new instance of Pimple -- and because Silex\Application extends Pimple all of this applies to Silex as well:
Listing 1 $container = new Pimple(); 3-3
Listing 3-4
or:
Listing = new Silex\Application(); 1 $app 3-5
Listing 3-6
Parameters
You can set parameters (which are usually strings) by setting an array key on the container:
Listing 1 $app['some_parameter'] = 'value'; 3-7
Listing 3-8
The array key can be anything, by convention periods are used for namespacing:
Listing 1 $app['asset.host'] = 'http://cdn.mysite.com/'; 3-9
Listing 3-10
Chapter 3: Services | 23
echo $app['some_parameter'];
Service definitions
Defining services is no different than defining parameters. You just set an array key on the container to be a closure. However, when you retrieve the service, the closure is executed. This allows for lazy service creation:
Listing Listing 1 3-13 3-14
$service = $app['some_service'];
Every time you call $app['some_service'], a new instance of the service is created.
Shared services
You may want to use the same instance of a service across all of your code. In order to do that you can make a shared service:
Listing Listing 1 3-17 3-18
This will create the service on first invocation, and then return the existing instance on any subsequent access.
Here you can see an example of Dependency Injection. some_service depends on some_other_service and takes some_service.config as configuration options. The dependency is only created when some_service is accessed, and it is possible to replace either of the dependencies by simply overriding those definitions.
Chapter 3: Services | 24
Protected closures
Because the container sees closures as factories for services, it will always execute them when reading them. In some cases you will however want to store a closure as a parameter, so that you can fetch it and execute it yourself -- with your own arguments. This is why Pimple allows you to protect your closures from being executed, by using the protect method:
1 2 3 4 5 6 7 8 9
Listing $app['closure_parameter'] = $app->protect(function ($a, $b) { 3-21 return $a + $b; });
Listing 3-22
// will not execute the closure $add = $app['closure_parameter']; // calling it now echo $add(2, 3);
Core services
Silex defines a range of services which can be used or replaced. You probably don't want to mess with most of them. request: Contains the current request object, which is an instance of Request2. It gives you access to GET, POST parameters and lots more! Example usage:
Listing = $app['request']->get('id'); 1 $id 3-23
Listing 3-24
This is only available when a request is being served, you can only access it from within a controller, before filter, after filter or error handler. routes: The RouteCollection3 that is used internally. You can add, modify, read routes. controllers: The Silex\ControllerCollection that is used internally. Check the Internals chapter for more information. dispatcher: The EventDispatcher4 that is used internally. It is the core of the Symfony2 system and is used quite a bit by Silex.
Chapter 3: Services | 25
resolver: The ControllerResolver5 that is used internally. It takes care of executing the controller with the right arguments. kernel: The HttpKernel6 that is used internally. The HttpKernel is the heart of Symfony2, it takes a Request as input and returns a Response as output. request_context: The request context is a simplified representation of the request that is used by the Router and the UrlGenerator. exception_handler: The Exception handler is the default handler that is used when you don't register one via the error() method or if your handler does not return a Response. Disable it with unset($app['exception_handler']). logger: A LoggerInterface7 instance. By default, logging is disabled as the value is set to null. When the Symfony2 Monolog bridge is installed, Monolog is automatically used as the default logger.
All of these Silex core services are shared.
Core parameters
request.http_port (optional): Allows you to override the default port for non-HTTPS URLs. If the current request is HTTP, it will always use the current port. Defaults to 80. This parameter can be used by the UrlGeneratorProvider. request.https_port (optional): Allows you to override the default port for HTTPS URLs. If the current request is HTTPS, it will always use the current port. Defaults to 443. This parameter can be used by the UrlGeneratorProvider. locale (optional): The locale of the user. When set before any request handling, it defines the default locale (en by default). When a request is being handled, it is automatically set according to the _locale request attribute of the current route. debug (optional): Returns whether or not the application is running in debug mode. Defaults to false. charset (optional): The charset to use for Responses. Defaults to UTF-8.
Chapter 3: Services | 26
Chapter 4
Providers
Providers allow the developer to reuse parts of an application into another one. Silex provides two types of providers defined by two interfaces: ServiceProviderInterface for services and ControllerProviderInterface for controllers.
Service Providers
Loading providers
In order to load and use a service provider, you must register it on the application:
Listing = new Silex\Application(); 1 $app 4-1 2 3 $app->register(new Acme\DatabaseServiceProvider());
Listing 4-2
You can also provide some parameters as a second argument. These will be set before the provider is registered:
Listing 1 $app->register(new Acme\DatabaseServiceProvider(), array( 4-3 2 'database.dsn' => 'mysql:host=localhost;dbname=myapp', 3 'database.user' => 'root', 4 'database.password' => 'secret_root_password', 5 ));
Listing 4-4
Conventions
You need to watch out in what order you do certain things when interacting with providers. Just keep to these rules: Overriding existing services must occur after the provider is registered.
PDF brought to you by generated on September 8, 2012 Chapter 4: Providers | 27
Reason: If the services already exist, the provider will overwrite it. You can set parameters any time before the service is accessed. Make sure to stick to this behavior when creating your own providers.
Included providers
There are a few provider that you get out of the box. All of these are within the Silex\Provider namespace: DoctrineServiceProvider MonologServiceProvider SessionServiceProvider SwiftmailerServiceProvider TwigServiceProvider TranslationServiceProvider UrlGeneratorServiceProvider ValidatorServiceProvider HttpCacheServiceProvider FormServiceProvider SecurityServiceProvider
Creating a provider
Providers must implement the Silex\ServiceProviderInterface:
This is very straight forward, just create a new class that implements the two methods. In the register() method, you can define services on the application which then may make use of other services and parameters. In the boot() method, you can configure the application, just before it handles a request. Here is an example of such a provider:
Listing Listing 1 4-7 4-8
1. https://github.com/fabpot/Silex/wiki/Third-Party-ServiceProviders
Chapter 4: Providers | 28
6 class HelloServiceProvider implements ServiceProviderInterface 7 { 8 public function register(Application $app) 9 { 10 $app['hello'] = $app->protect(function ($name) use ($app) { 11 $default = $app['hello.default_name'] ? $app['hello.default_name'] : ''; 12 $name = $name ?: $default; 13 14 return 'Hello '.$app->escape($name); 15 }); 16 } 17 18 public function boot(Application $app) 19 { 20 } 21 }
This class provides a hello service which is a protected closure. It takes a name argument and will return hello.default_name if no name is given. If the default is also missing, it will use an empty string. You can now use this provider as follows:
1 2 3 4 5 6 7 8 9 10 11
Listing = new Silex\Application(); $app 4-9
Listing 4-10
$app->register(new Acme\HelloServiceProvider(), array( 'hello.default_name' => 'Igor', )); $app->get('/hello', function () use ($app) { $name = $app['request']->get('name'); return $app['hello']($name); });
In this example we are getting the name parameter from the query string, so the request path would have to be /hello?name=Fabien.
Controllers providers
Loading providers
In order to load and use a controller provider, you must "mount" its controllers under a path:
Listing = new Silex\Application(); 1 $app 4-11 2 3 $app->mount('/blog', new Acme\BlogControllerProvider());
Listing 4-12
All controllers defined by the provider will now be available under the /blog path.
Chapter 4: Providers | 29
Creating a provider
Providers must implement the Silex\ControllerProviderInterface:
namespace Acme; use Silex\Application; use Silex\ControllerProviderInterface; use Silex\ControllerCollection; class HelloControllerProvider implements ControllerProviderInterface { public function connect(Application $app) { // creates a new controller based on the default route $controllers = $app['controllers_factory']; $controllers->get('/', function (Application $app) { return $app->redirect('/hello'); }); return $controllers; } }
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
The connect method must return an instance of ControllerCollection. ControllerCollection is the class where all controller related methods are defined (like get, post, match, ...).
The Application class acts in fact as a proxy for these methods.
In this example, the /blog/ path now references the controller defined in the provider.
You can also define a provider that implements both the service and the controller provider interface and package in the same class the services needed to make your controllers work.
Chapter 4: Providers | 30
Chapter 5
Testing
Because Silex is built on top of Symfony2, it is very easy to write functional tests for your application. Functional tests are automated software tests that ensure that your code is working correctly. They go through the user interface, using a fake browser, and mimic the actions a user would do.
Why
If you are not familiar with software tests, you may be wondering why you would need this. Every time you make a change to your application, you have to test it. This means going through all the pages and making sure they are still working. Functional tests save you a lot of time, because they enable you to test your application in usually under a second by running a single command. For more information on functional testing, unit testing, and automated software tests in general, check out PHPUnit1 and Bulat Shakirzyanov's talk on Clean Code.
PHPUnit
PHPUnit2 is the de-facto standard testing framework for PHP. It was built for writing unit tests, but it can be used for functional tests too. You write tests by creating a new class, that extends the PHPUnit_Framework_TestCase. Your test cases are methods prefixed with test:
Listing 1 class ContactFormTest extends PHPUnit_Framework_TestCase 5-1 2 { 3 public function testInitialPage() 4 { 5 ... 6 } 7 }
Listing 5-2
1. https://github.com/sebastianbergmann/phpunit 2. https://github.com/sebastianbergmann/phpunit
Chapter 5: Testing | 31
In your test cases, you do assertions on the state of what you are testing. In this case we are testing a contact form, so we would want to assert that the page loaded correctly and contains our form:
Listing Listing 1 5-3 5-4
public function testInitialPage() 2 { 3 $statusCode = ... 4 $pageContent = ... 5 6 $this->assertEquals(200, $statusCode); 7 $this->assertContains('Contact us', $pageContent); 8 $this->assertContains('<form', $pageContent); 9 }
Here you see some of the available assertions. There is a full list available in the Writing Tests for PHPUnit3 section of the PHPUnit documentation.
WebTestCase
Symfony2 provides a WebTestCase class that can be used to write functional tests. The Silex version of this class is Silex\WebTestCase, and you can use it by making your test extend it:
To make your application testable, you need to make sure you follow "Reusing applications" instructions from Usage.
For your WebTestCase, you will have to implement a createApplication method, which returns your application. It will probably look like this:
Listing Listing 1 5-7 5-8
Make sure you do not use require_once here, as this method will be executed before every test.
By default, the application behaves in the same way as when using it from a browser. But when an error occurs, it is sometimes easier to get raw exceptions instead of HTML pages. It is rather simple if you tweak the application configuration in the createApplication() method like follows:
3. http://www.phpunit.de/manual/current/en/writing-tests-for-phpunit.html
Chapter 5: Testing | 32
Listing 1 public function createApplication() 2 { 5-9 3 $app = require __DIR__.'/path/to/app.php'; 4 $app['debug'] = true; 5 unset($app['exception_handler']); 6 7 return $app; 8 }
Listing 5-10
Listing 5-12
The WebTestCase provides a createClient method. A client acts as a browser, and allows you to interact with your application. Here's how it works:
Listing 1 public function testInitialPage() 5-13 2 { 3 $client = $this->createClient(); 4 $crawler = $client->request('GET', '/'); 5 6 $this->assertTrue($client->getResponse()->isOk()); 7 $this->assertCount(1, $crawler->filter('h1:contains("Contact us")')); 8 $this->assertCount(1, $crawler->filter('form')); 9 ... 10 }
Listing 5-14
There are several things going on here. You have both a Client and a Crawler. You can also access the application through $this->app.
Client
The client represents a browser. It holds your browsing history, cookies and more. The request method allows you to make a request to a page on your application.
Chapter 5: Testing | 33
You can find some documentation for it in the client section of the testing chapter of the Symfony2 documentation.
Crawler
The crawler allows you to inspect the content of a page. You can filter it using CSS expressions and lots more.
You can find some documentation for it in the crawler section of the testing chapter of the Symfony2 documentation.
Configuration
The suggested way to configure PHPUnit is to create a phpunit.xml.dist file, a tests folder and your tests in tests/YourApp/Tests/YourTest.php. The phpunit.xml.dist file should look like this:
<?xml version="1.0" encoding="UTF-8"?> 2 <phpunit backupGlobals="false" 3 backupStaticAttributes="false" 4 colors="true" 5 convertErrorsToExceptions="true" 6 convertNoticesToExceptions="true" 7 convertWarningsToExceptions="true" 8 processIsolation="false" 9 stopOnFailure="false" 10 syntaxCheck="false" 11 > 12 <testsuites> 13 <testsuite name="YourApp Test Suite"> 14 <directory>./tests/</directory> 15 </testsuite> 16 </testsuites> 17 </phpunit>
You can also configure a bootstrap file for autoloading and whitelisting for code coverage reports. Your tests/YourApp/Tests/YourTest.php should look like this:
namespace YourApp\Tests; 2 3 use Silex\WebTestCase; 4 5 class YourTest extends WebTestCase 6 { 7 public function createApplication() 8 { 9 return require __DIR__.'/../../../app.php';
Chapter 5: Testing | 34
10 11 12 13 14 15 16 }
Now, when running phpunit on the command line, your tests should run.
Chapter 5: Testing | 35
Chapter 6
Example API
In this example we will create an API for creating a blog post. The following is a spec of how we want it to work.
Request
In the request we send the data for the blog post as a JSON object. We also indicate that using the Content-Type header:
2 3 4 5 6
POST /blog/posts Accept: application/json Content-Type: application/json Content-Length: 57 {"title":"Hello World!","body":"This is my first post!"}
Response
The server responds with a 201 status code, telling us that the post was created. It tells us the ContentType of the response, which is also JSON:
1 2 3 4 5 6 7 8 9
Listing 6-6
$app->before(function (Request $request) { if (0 === strpos($request->headers->get('Content-Type'), 'application/json')) { $data = json_decode($request->getContent(), true); $request->request->replace(is_array($data) ? $data : array()); } });
Controller implementation
Our controller will create a new blog post from the data provided and will return the post object, including its id, as JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13
Listing 6-8
$app->post('/blog/posts', function (Request $request) use ($app) { $post = array( 'title' => $request->request->get('title'), 'body' => $request->request->get('body'), ); $post['id'] = createPost($post); return $app->json($post, 201); });
Manual testing
In order to manually test our API, we can use the curl command line utility, which allows sending HTTP requests:
$ curl http://blog.lo/blog/posts -d '{"title":"Hello World!","body":"This is my first 2 post!"}' -H 'Content-Type: application/json' {"id":"1","title":"Hello World!","body":"This is my first post!"}
Chapter 7
Listing 7-2
$app->before(function () use ($app) { $app['translator']->addLoader('xlf', new Symfony\Component\Translation\Loader\XliffFileLoader()); $app['translator']->addResource('xlf', __DIR__.'/vendor/symfony/src/Symfony/Bundle/ FrameworkBundle/Resources/translations/validators.sr_Latn.xlf', 'sr_Latn', 'validators'); });
And that's all you need to load translations from Symfony2 xlf files.
Chapter 8
Example
Listing Listing 1 8-1 8-2
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; $app->register(new Silex\Provider\SessionServiceProvider()); $app['pdo.dsn'] = 'mysql:dbname=mydatabase'; $app['pdo.user'] = 'myuser'; $app['pdo.password'] = 'mypassword'; $app['pdo.db_options'] 'db_table' => 'db_id_col' => 'db_data_col' => 'db_time_col' => ); = array( 'session', 'session_id', 'session_value', 'session_time',
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
1. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.html 2. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.html
18 $app['pdo.dsn'], 19 $app['pdo.user'], 20 $app['pdo.password'] 21 ); 22 }); 23 24 $app['session.storage.handler'] = $app->share(function () use ($app) { 25 return new PdoSessionHandler( 26 $app['pdo'], 27 $app['pdo.db_options'], 28 $app['session.storage.options'] 29 ); 30 });
Database structure
PdoSessionStorage needs a database table with 3 columns: session_id: ID column (VARCHAR(255) or larger) session_value: Value column (TEXT or CLOB) session_time: Time column (INTEGER) You can find examples of SQL statements to create the session table in the Symfony2 cookbook3
3. http://symfony.com/doc/current/cookbook/configuration/pdo_session_storage.html
Chapter 9
Example
Listing Listing 1 9-1 9-2
That's it, your form could be submitted from everywhere without CSRF Protection.
Going further
This specific example showed how to change the csrf_protection in the $options parameter of the createBuilder() function. More of them could be passed through this parameter, it is as simple as using the Symfony2 getDefaultOptions() method in your form classes. See more here2.
1. http://symfony.com/doc/current/book/forms.html#csrf-protection 2. http://symfony.com/doc/current/book/forms.html#book-form-creating-form-classes
Chapter 10
Next, you need to tell the Validation Service that you are not using StaticMethodLoader to load your class metadata but a YAML file:
Listing 1 $app->register(new ValidatorServiceProvider()); 10-2 2 3 $app['validator.mapping.class_metadata_factory'] = new 4 Symfony\Component\Validator\Mapping\ClassMetadataFactory( 5 new Symfony\Component\Validator\Mapping\Loader\YamlFileLoader(__DIR__.'/validation.yml') );
Listing 10-3
Now, we can replace the usage of the static method and move all the validation rules to validation.yml:
Listing 1 # validation.yml 10-4 2 Post: 3 properties: 4 title: 5 - NotNull: ~ 6 - NotBlank: ~ 7 body: 8 - Min: 100
Listing 10-5
Chapter 11
Internals
This chapter will tell you a bit about how Silex works internally.
Silex
Application
The application is the main interface to Silex. It implements Symfony2's HttpKernelInterface1, so you can pass a Request2 to the handle method and it will return a Response3. It extends the Pimple service container, allowing for flexibility on the outside as well as the inside. You could replace any service, and you are also able to read them. The application makes strong use of the EventDispatcher4 to hook into the Symfony2 HttpKernel5 events. This allows fetching the Request, converting string responses into Response objects and handling Exceptions. We also use it to dispatch some custom events like before/after filters and errors.
Controller
The Symfony2 Route6 is actually quite powerful. Routes can be named, which allows for URL generation. They can also have requirements for the variable parts. In order to allow settings these through a nice interface the match method (which is used by get, post, etc.) returns an instance of the Controller, which wraps a route.
ControllerCollection
One of the goals of exposing the RouteCollection7 was to make it mutable, so providers could add stuff to it. The challenge here is the fact that routes know nothing about their name. The name only has meaning in context of the RouteCollection and cannot be changed. To solve this challenge we came up with a staging area for routes. The ControllerCollection holds the controllers until flush is called, at which point the routes are added to the RouteCollection. Also, the controllers are then frozen. This means that they can no longer be modified and will throw an Exception if you try to do so. Unfortunately no good way for flushing implicitly could be found, which is why flushing is now always explicit. The Application will flush, but if you want to read the ControllerCollection before the request takes place, you will have to call flush yourself. The Application provides a shortcut flush method for flushing the ControllerCollection.
Symfony2
Following Symfony2 components are used by Silex: HttpFoundation: For Request and Response. HttpKernel: Because we need a heart. Routing: For matching defined routes. EventDispatcher: For hooking into the HttpKernel.
7. http://api.symfony.com/master/Symfony/Component/Routing/RouteCollection.html 8. http://symfony.com/
Chapter 12
Contributing
We are open to contributions to the Silex code. If you find a bug or want to contribute a provider, just follow these steps. Fork the Silex repository1 on github. Make your feature addition or bug fix. Add tests for it. This is important so we don't break it in a future version unintentionally. Send a pull request. Bonus points for topic branches.
If you have a big change or would like to discuss something, please join us on the mailing list2.
Any code you contribute must be licensed under the MIT License.
1. https://github.com/fabpot/Silex 2. http://groups.google.com/group/silex-php
Chapter 13
DoctrineServiceProvider
The DoctrineServiceProvider provides integration with the Doctrine DBAL1 for easy database acccess.
There is only a Doctrine DBAL. An ORM service is not supplied.
Parameters
db.options: Array of Doctrine DBAL options. These options are available: driver: The database driver to use, defaults to pdo_mysql. Can be any of: pdo_mysql, pdo_sqlite, pdo_pgsql, pdo_oci, oci8, ibm_db2, pdo_ibm, pdo_sqlsrv. dbname: The name of the database to connect to. host: The host of the database to connect to. Defaults to localhost. user: The user of the database to connect to. Defaults to root. password: The password of the database to connect to. path: Only relevant for pdo_sqlite, specifies the path to the SQLite database. These and additional options are described in detail in the Doctrine DBAL configuration documentation.
Services
db: The database connection, instance of Doctrine\DBAL\Connection. db.config: Configuration object for Doctrine. Defaults Doctrine\DBAL\Configuration.
1. http://www.doctrine-project.org/projects/dbal
to
an
empty
Registering
Listing Listing 1 13-1 13-2
$app->register(new Silex\Provider\DoctrineServiceProvider(), array( 2 'db.options' => array( 3 'driver' => 'pdo_sqlite', 4 'path' => __DIR__.'/app.db', 5 ), 6 ));
Doctrine DBAL comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
Listing 13-3
Usage
The Doctrine provider provides a db service. Here is a usage example:
$app->get('/blog/show/{id}', function ($id) use ($app) { 2 $sql = "SELECT * FROM posts WHERE id = ?"; 3 $post = $app['db']->fetchAssoc($sql, array((int) $id)); 4 5 return "<h1>{$post['title']}</h1>". 6 "<p>{$post['body']}</p>"; 7 });
2 3 4 5 6 7 8
$app->register(new Silex\Provider\DoctrineServiceProvider(), array( 'dbs.options' => array ( 'mysql_read' => array( 'driver' => 'pdo_mysql', 'host' => 'mysql_read.someplace.tld', 'dbname' => 'my_database', 'user' => 'my_username', 'password' => 'my_password',
9 10 11 12 13 14 15 16 17 ), 18 ));
), 'mysql_write' => array( 'driver' => 'pdo_mysql', 'host' => 'mysql_write.someplace.tld', 'dbname' => 'my_database', 'user' => 'my_username', 'password' => 'my_password', ),
The first registered connection is the default and can simply be accessed as you would if there was only one connection. Given the above configuration, these two lines are equivalent:
Listing 1 $app['db']->fetchAssoc('SELECT * FROM table'); 13-8 2 3 $app['dbs']['mysql_read']->fetchAssoc('SELECT * FROM table');
Listing 13-9
Listing 13-11
2. http://www.doctrine-project.org/docs/dbal/2.0/en/
Chapter 14
MonologServiceProvider
The MonologServiceProvider provides a default logging mechanism through Jordi Boggiano's Monolog1 library. It will log requests and errors and allow you to add debug logging to your application, so you don't have to use var_dump so much anymore. You can use the grown-up version called tail -f.
Parameters
monolog.logfile: File where logs are written to. monolog.level (optional): Level of logging defaults to DEBUG. Must be one of Logger::DEBUG, Logger::INFO, Logger::WARNING, Logger::ERROR. DEBUG will log everything, INFO will log everything except DEBUG, etc. monolog.name (optional): Name of the monolog channel, defaults to myapp.
Services
monolog: The monolog logger instance. Example usage:
Listing Listing 1 14-1 14-2
1. https://github.com/Seldaek/monolog
Registering
Listing 1 $app->register(new Silex\Provider\MonologServiceProvider(), array( 14-3 2 'monolog.logfile' => __DIR__.'/development.log', 3 )); Listing 14-4
Monolog comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
"require": { "monolog/monolog": ">=1.0.0", }
Listing 14-5
Usage
The MonologServiceProvider provides a monolog service. You can use it to add log entries for any logging level through addDebug(), addInfo(), addWarning() and addError():
Listing Symfony\Component\HttpFoundation\Response; 1 use 14-6 2 3 $app->post('/user', function () use ($app) { 4 // ... 5 6 $app['monolog']->addInfo(sprintf("User '%s' registered.", $username)); 7 8 return new Response('', 201); 9 });
Listing 14-7
Customization
You can configure Monolog (like adding or changing the handlers) before using it by extending the monolog service:
Listing 1 $app['monolog'] = $app->share($app->extend('monolog', function($monolog, $app) { 14-8 2 $monolog->pushHandler(...); 3 4 return $monolog; 5 }));
Listing 14-9
Traits
Silex\Application\MonologTrait adds the following shortcuts: log: Logs a message.
PDF brought to you by generated on September 8, 2012 Chapter 14: MonologServiceProvider | 51
2. https://github.com/Seldaek/monolog
Chapter 15
SessionServiceProvider
The SessionServiceProvider provides a service for storing data persistently between requests.
Parameters
session.storage.save_path (optional): The path for the FileSessionHandler, defaults to the value of sys_get_temp_dir(). session.storage.options: An array of options that is passed to the constructor of the session.storage service. In case of the default NativeSessionStorage1, the possible options are: name: The cookie name (_SESS by default) id: The session id (null by default) cookie_lifetime: Cookie lifetime path: Cookie path domain: Cookie domain secure: Cookie secure (HTTPS) httponly: Whether the cookie is http only
However, all of these are optional. Sessions last as long as the browser is open. To override this, set the lifetime option. session.test: Whether to simulate sessions or not (useful when writing functional tests).
Services
session: An instance of Symfony2's Session2. session.storage: A service that is used for persistence of the session data. session.storage.handler: A service that is used by the session.storage for data access. Defaults to a FileSessionHandler3 storage handler.
1. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.html 2. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Session/Session.html
Registering
Listing Listing 1 15-1 15-2
$app->register(new Silex\Provider\SessionServiceProvider());
Usage
The Session provider provides a session service. Here is an example that authenticates a user and creates a session for him:
Listing Listing 1 15-3 15-4
use Symfony\Component\HttpFoundation\Response; $app->get('/login', function () use ($app) { $username = $app['request']->server->get('PHP_AUTH_USER', false); $password = $app['request']->server->get('PHP_AUTH_PW'); if ('igor' === $username && 'password' === $password) { $app['session']->set('user', array('username' => $username)); return $app->redirect('/account'); } $response = new Response(); $response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', 'site_login')); $response->setStatusCode(401, 'Please sign in.'); return $response; }); $app->get('/account', function () use ($app) { if (null === $user = $app['session']->get('user')) { return $app->redirect('/login'); } return "Welcome {$user['username']}!"; });
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
3. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Session/Storage/Handler/FileSessionHandler.html
Chapter 16
SwiftmailerServiceProvider
The SwiftmailerServiceProvider provides a service for sending email through the Swift Mailer1 library. You can use the mailer service to send messages easily. By default, it will attempt to send emails through SMTP.
Parameters
swiftmailer.options: An array of options for the default SMTP-based configuration. The following options can be set: host: SMTP hostname, defaults to 'localhost'. port: SMTP port, defaults to 25. username: SMTP username, defaults to an empty string. password: SMTP password, defaults to an empty string. encryption: SMTP encryption, defaults to null. auth_mode: SMTP authentication mode, defaults to null.
Services
mailer: The mailer instance. Example usage:
Listing 1 $message = \Swift_Message::newInstance(); 16-1 2 3 // ... 4 5 $app['mailer']->send($message);
Listing 16-2
1. http://swiftmailer.org
used
for
delivery.
Defaults
to
swiftmailer.transport.buffer: StreamBuffer used by the transport. swiftmailer.transport.authhandler: Authentication handler used by the transport. Will try the following by default: CRAM-MD5, login, plaintext. swiftmailer.transport.eventdispatcher: Internal event dispatcher used by Swiftmailer.
Registering
Listing Listing 1 16-3 16-4
$app->register(new Silex\Provider\SwiftmailerServiceProvider());
SwiftMailer comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
Listing 16-5
Usage
The Swiftmailer provider provides a mailer service:
$app->post('/feedback', function () use ($app) { 2 $request = $app['request']; 3 4 $message = \Swift_Message::newInstance() 5 ->setSubject('[YourSite] Feedback') 6 ->setFrom(array('[email protected]')) 7 ->setTo(array('[email protected]')) 8 ->setBody($request->get('message')); 9 10 $app['mailer']->send($message); 11 12 return new Response('Thank you for your feedback!', 201); 13 });
Traits
Silex\Application\SwiftmailerTrait adds the following shortcuts: mail: Sends an email.
2. http://swiftmailer.org
Chapter 17
TranslationServiceProvider
The TranslationServiceProvider provides a service for translating your application into different languages.
Parameters
translator.domains (optional): A mapping of domains/locales/messages. This parameter contains the translation data for all languages and domains. locale (optional): The locale for the translator. You will most likely want to set this based on some request parameter. Defaults to en. locale_fallback (optional): Fallback locale for the translator. It will be used when the current locale has no messages set.
Services
translator: An instance of Translator1, that is used for translation. translator.loader: An instance of an implementation of the translation LoaderInterface2, defaults to an ArrayLoader3. translator.message_selector: An instance of MessageSelector4.
Registering
Listing Listing 17-1 17-2
The Symfony Translation Component comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
"require": { "symfony/translation": "2.1.*" }
Listing 17-3
Usage
The Translation provider provides a translator service and makes use of the translator.domains parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Listing $app['translator.domains'] = array( 17-4 'messages' => array( 'en' => array( 'hello' => 'Hello %name%', 'goodbye' => 'Goodbye %name%', ), 'de' => array( 'hello' => 'Hallo %name%', 'goodbye' => 'Tschss %name%', ), 'fr' => array( 'hello' => 'Bonjour %name%', 'goodbye' => 'Au revoir %name%', ), ), 'validators' => array( 'fr' => array( 'This value should be a valid number.' => 'Cette valeur doit tre un nombre.', ), ), );
Listing 17-5
$app->get('/{_locale}/{message}/{name}', function ($message, $name) use ($app) { return $app['translator']->trans($message, array('%name%' => $name)); });
The above example will result in following routes: /en/hello/igor will return Hello igor. /de/hello/igor will return Hallo igor. /fr/hello/igor will return Bonjour igor. /it/hello/igor will return Hello igor (because of the fallback).
Traits
Silex\Application\TranslationTrait adds the following shortcuts: trans: Translates the given message. transChoice: Translates the given choice message by choosing a translation according to a number.
Listing Listing 1 17-6 17-7
Recipes
YAML-based language files
Having your translations in PHP files can be inconvenient. This recipe will show you how to load translations from external YAML files. First, add the Symfony2 Config and Yaml components in your composer file:
Listing 17-8
Next, you have to create the language mappings in YAML files. A naming you can use is locales/ en.yml. Just do the mapping in this file as follows:
Then, register the YamlFileLoader on the translator and add all your translation files:
use Symfony\Component\Translation\Loader\YamlFileLoader; 2 3 $app['translator'] = $app->share($app->extend('translator', function($translator, $app) { 4 $translator->addLoader('yaml', new YamlFileLoader()); 5 6 $translator->addResource('yaml', __DIR__.'/locales/en.yml', 'en'); 7 $translator->addResource('yaml', __DIR__.'/locales/de.yml', 'de'); 8 $translator->addResource('yaml', __DIR__.'/locales/fr.yml', 'fr'); 9 10 return $translator; 11 }));
Listing 17-14
Listing 17-16
Moreover, when using the Twig bridge provided by Symfony (see TwigServiceProvider), you will be allowed to translate strings in the Twig way:
Listing 1 {{ 'translation_key'|trans }} 17-17 2 {{ 'translation_key'|transchoice }} 3 {% trans %}translation_key{% endtrans %}
Listing 17-18
Chapter 18
TwigServiceProvider
The TwigServiceProvider provides integration with the Twig1 template engine.
Parameters
twig.path (optional): Path to the directory containing twig template files (it can also be an array of paths). twig.templates (optional): An associative array of template names to template contents. Use this if you want to define your templates inline. twig.options (optional): An associative array of twig options. Check out the twig documentation for more information. twig.form.templates (optional): An array of templates used to render forms (only available when the FormServiceProvider is enabled).
Services
twig: The Twig_Environment instance. The main way of interacting with Twig. twig.loader: The loader for Twig templates which uses the twig.path and the twig.templates options. You can also replace the loader completely.
Registering
Listing Listing 1 18-1 18-2
1. http://twig.sensiolabs.org/
Twig comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
"require": { "twig/twig": ">=1.8,<2.0-dev" }
Listing 18-3
When present, the TwigServiceProvider will provide you with the following additional capabilities: UrlGeneratorServiceProvider: If you are using the UrlGeneratorServiceProvider, you will have access to the path() and url() functions. You can find more information in the Symfony2 Routing documentation. TranslationServiceProvider: If you are using the TranslationServiceProvider, you will get the trans() and transchoice() functions for translation in Twig templates. You can find more information in the Symfony2 Translation documentation2. FormServiceProvider: If you are using the FormServiceProvider, you will get a set of helpers for working with forms in templates. You can find more information in the Symfony2 Forms reference3. SecurityServiceProvider: If you are using the SecurityServiceProvider, you will have access to the is_granted() function in templates. You can find more information in the Symfony2 Security documentation.
Usage
The Twig provider provides a twig service:
Listing 1 $app->get('/hello/{name}', function ($name) use ($app) { 18-5 2 return $app['twig']->render('hello.twig', array( 3 'name' => $name, 4 )); 5 });
Listing 18-6
This will render a file named views/hello.twig. In any Twig template, the app variable refers to the Application object. So you can access any services from within your view. For example to access $app['request']->getHost(), just put this in your template:
2. http://symfony.com/doc/current/book/translation.html#twig-templates 3. http://symfony.com/doc/current/reference/forms/twig_reference.html
{{ app.request.host }}
A render function is also registered to help you render another controller from a template:
{{ render('/sidebar') }} 2 3 {# or if you are also using UrlGeneratorServiceProvider with the 4 SymfonyBridgesServiceProvider #} {{ render(path('sidebar')) }}
Traits
Silex\Application\TwigTrait adds the following shortcuts: render: Renders a view with the given parameters and returns a Response object.
Listing Listing 1 18-11 18-12
return $app->render('index.html', ['name': 'Fabien']); 2 3 $response = new Response(); 4 $response->setTtl(10); 5 6 return $app->render('index.html', ['name': 'Fabien'], $response);
// stream a view 2 use Symfony\Component\HttpFoundation\StreamedResponse; 3 4 return $app->render('index.html', ['name': 'Fabien'], new StreamedResponse());
Customization
You can configure the Twig environment before using it by extending the twig service:
$app['twig'] = $app->share($app->extend('twig', function($twig, $app) { 2 $twig->addGlobal('pi', 3.14); 3 $twig->addFilter('levenshtein', new \Twig_Filter_Function('levenshtein')); 4 5 return $twig; 6 }));
4. http://twig.sensiolabs.org
Chapter 19
UrlGeneratorServiceProvider
The UrlGeneratorServiceProvider provides a service for generating URLs for named routes.
Parameters
None.
Services
url_generator: An instance of UrlGenerator1, using the RouteCollection2 that is provided through the routes service. It has a generate method, which takes the route name as an argument, followed by an array of route parameters.
Registering
Listing 1 $app->register(new Silex\Provider\UrlGeneratorServiceProvider()); 19-1 Listing 19-2
Usage
The UrlGenerator provider provides a url_generator service:
Listing 19-3
Listing 19-4
1. http://api.symfony.com/master/Symfony/Component/Routing/Generator/UrlGenerator.html 2. http://api.symfony.com/master/Symfony/Component/Routing/RouteCollection.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
$app->get('/', function () { return 'welcome to the homepage'; }) ->bind('homepage'); $app->get('/hello/{name}', function ($name) { return "Hello $name!"; }) ->bind('hello'); $app->get('/navigation', function () use ($app) { return '<a href="'.$app['url_generator']->generate('homepage').'">Home</a>'. ' | '. '<a href="'.$app['url_generator']->generate('hello', array('name' => 'Igor')).'">Hello Igor</a>'; });
{{ app.url_generator.generate('homepage') }}
Moreover, if you use Twig, you will have access to the path() and url() functions:
Traits
Silex\Application\UrlGeneratorTrait adds the following shortcuts: path: Generates a path. url: Generates an absolute URL.
Listing Listing 1 19-919-10
$app->path('homepage'); 2 $app->url('homepage');
Chapter 20
ValidatorServiceProvider
The ValidatorServiceProvider provides a service for validating data. It is most useful when used with the FormServiceProvider, but can also be used standalone.
Parameters
none
Services
validator: An instance of Validator1. validator.mapping.class_metadata_factory: Factory for metadata loaders, which can read validation constraint information from classes. Defaults to StaticMethodLoader-ClassMetadataFactory. This means you can define a static loadValidatorMetadata method on your data class, which takes a ClassMetadata argument. Then you can set constraints on this ClassMetadata instance. validator.validator_factory: Factory for ConstraintValidators. Defaults to a standard ConstraintValidatorFactory. Mostly used internally by the Validator.
Registering
Listing 1 $app->register(new Silex\Provider\ValidatorServiceProvider()); 20-1 Listing 20-2
1. http://api.symfony.com/master/Symfony/Component/Validator/Validator.html
The Symfony Validator Component comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
Listing 20-3
Usage
The Validator provider provides a validator service.
Validating Values
You can validate values directly using the validateValue validator method:
use Symfony\Component\Validator\Constraints as Assert; 2 3 $app->get('/validate/{email}', function ($email) use ($app) { 4 $errors = $app['validator']->validateValue($email, new Assert\Email()); 5 6 if (count($errors) > 0) { 7 return (string) $errors; 8 } else { 9 return 'The email is valid'; 10 } 11 });
use Symfony\Component\Validator\Constraints as Assert; class Book { public $title; public $author; } class Author { public $first_name; public $last_name; } $book = array( 'title' => 'My Book', 'author' => array( 'first_name' => 'Fabien', 'last_name' => 'Potencier',
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
), ); $constraint = new Assert\Collection(array( 'title' => new Assert\MinLength(10), 'author' => new Assert\Collection(array( 'first_name' => array(new Assert\NotBlank(), new Assert\MinLength(10)), 'last_name' => new Assert\MinLength(10), )), )); $errors = $app['validator']->validateValue($book, $constraint); if (count($errors) > 0) { foreach ($errors as $error) { echo $error->getPropertyPath().' '.$error->getMessage()."\n"; } } else { echo 'The book is valid'; }
Validating Objects
If you want to add validations to a class, you can define the constraint for the class properties and getters, and then call the validate method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
Listing 20-9
$author = new Author(); $author->first_name = 'Fabien'; $author->last_name = 'Potencier'; $book = new Book(); $book->title = 'My Book'; $book->author = $author; $metadata = $app['validator.mapping.class_metadata_factory']->getClassMetadata('Author'); $metadata->addPropertyConstraint('first_name', new Assert\NotBlank()); $metadata->addPropertyConstraint('first_name', new Assert\MinLength(10)); $metadata->addPropertyConstraint('last_name', new Assert\MinLength(10)); $metadata = $app['validator.mapping.class_metadata_factory']->getClassMetadata('Book'); $metadata->addPropertyConstraint('title', new Assert\MinLength(10)); $metadata->addPropertyConstraint('author', new Assert\Valid()); $errors = $app['validator']->validate($book); if (count($errors) > 0) { foreach ($errors as $error) { echo $error->getPropertyPath().' '.$error->getMessage()."\n"; } } else { echo 'The author is valid'; }
You can also declare the class constraint by adding a static loadValidatorMetadata method to your classes:
Listing Listing 1 20-10 20-11
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; class Book { public $title; public $author; static public function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('title', new Assert\MinLength(10)); $metadata->addPropertyConstraint('author', new Assert\Valid()); } } class Author { public $first_name; public $last_name; static public function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('first_name', new Assert\NotBlank()); $metadata->addPropertyConstraint('first_name', new Assert\MinLength(10)); $metadata->addPropertyConstraint('last_name', new Assert\MinLength(10)); } } $app->get('/validate/{email}', function ($email) use ($app) { $author = new Author(); $author->first_name = 'Fabien'; $author->last_name = 'Potencier'; $book = new Book(); $book->title = 'My Book'; $book->author = $author; $errors = $app['validator']->validate($book); if (count($errors) > 0) { foreach ($errors as $error) { echo $error->getPropertyPath().' '.$error->getMessage()."\n"; } } else { echo 'The author is valid'; } });
Use addGetterConstraint() to add constraints on getter methods and addConstraint() to add constraints on the class itself.
Translation
To be able to translate the error messages, you can use the translator provider and register the messages under the validators domain:
Listing 1 $app['translator.domains'] = array( 20-12 2 'validators' => array( 3 'fr' => array( 4 'This value should be a valid number.' => 'Cette valeur doit tre un nombre.', 5 ), 6 ), 7 );
Listing 20-13
2. http://symfony.com/doc/2.0/book/validation.html
Chapter 21
FormServiceProvider
The FormServiceProvider provides a service for building forms in your application with the Symfony2 Form component.
Parameters
form.secret: This secret value is used for generating and validating the CSRF token for a specific page. It is very important for you to set this value to a static randomly generated value, to prevent hijacking of your forms. Defaults to md5(__DIR__).
Services
form.factory: An instance of FormFactory1, that is used for build a form. form.csrf_provider: An instance of an implementation of the CsrfProviderInterface2, defaults to a DefaultCsrfProvider3.
Registering
Listing Listing 1 21-1 21-2
If you don't want to create your own form layout, it's fine: a default one will be used. But you will have to register the translation provider as the default form layout requires it. If you want to use validation with forms, do not forget to register the Validator provider.
The Symfony Form Component and all its dependencies (optional or not) comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
"require": { "symfony/form": "2.1.*" }
Listing 21-3
If you are going to use the validation extension with forms, you must also add a dependency to the symfony/config and `symfony/translation components:
"require": { "symfony/config": "2.1.*", "symfony/translation": "2.1.*" }
Listing 21-4
The Symfony Form Component relies on the PHP intl extension. If you don't have it, you can install the Symfony Locale Component as a replacement:
"require": { "symfony/locale": "2.1.*" }
Listing 21-5
Usage
The FormServiceProvider provides a form.factory service. Here is a usage example:
Listing 1 $app->match('/form', function (Request $request) use ($app) { 21-6 2 // some default data for when the form is displayed the first time 3 $data = array( 4 'name' => 'Your name', 5 'email' => 'Your email', 6 ); 7 8 $form = $app['form.factory']->createBuilder('form', $data) 9 ->add('name') 10 ->add('email') 11 ->add('gender', 'choice', array( 12 'choices' => array(1 => 'male', 2 => 'female'), 13 'expanded' => true, 14 )) 15 ->getForm(); 16 17 if ('POST' == $request->getMethod()) { 18 $form->bind($request); 19 20 if ($form->isValid()) {
Listing 21-7
21 $data = $form->getData(); 22 23 // do something with the data 24 25 // redirect somewhere 26 return $app->redirect('...'); 27 } 28 } 29 30 // display the form 31 return $app['twig']->render('index.twig', array('form' => $form->createView())); 32 });
If you are using the validator provider, you can also add validation to your form by adding constraints on the fields:
Listing Listing 1 21-10 21-11
use Symfony\Component\Validator\Constraints as Assert; $app->register(new Silex\Provider\ValidatorServiceProvider()); $app->register(new Silex\Provider\TranslationServiceProvider(), array( 'translator.messages' => array(), )); $form = $app['form.factory']->createBuilder('form') ->add('name', 'text', array( 'constraints' => array(new Assert\NotBlank(), new Assert\MinLength(5)) )) ->add('email', 'text', array( 'constraints' => new Assert\Email() )) ->add('gender', 'choice', array( 'choices' => array(1 => 'male', 2 => 'female'), 'expanded' => true, 'constraints' => new Assert\Choice(array(1, 2)), )) ->getForm();
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Traits
Silex\Application\FormTrait adds the following shortcuts: form: Creates a FormBuilder instance.
Listing 1 $app->form('form', $data); 21-14
Listing 21-15
4. http://symfony.com/doc/2.1/book/forms.html
Chapter 22
HttpCacheServiceProvider
The HttpCacheProvider provides support for the Symfony2 Reverse Proxy.
Parameters
http_cache.cache_dir: The cache directory to store the HTTP cache data. http_cache.options (optional): An array of options for the HttpCache1 constructor.
Services
http_cache: An instance of HttpCache2.
Registering
Listing Listing 1 22-1 22-2
Usage
Silex already supports any reverse proxy like Varnish out of the box by setting Response HTTP cache headers:
1. http://api.symfony.com/master/Symfony/Component/HttpKernel/HttpCache/HttpCache.html 2. http://api.symfony.com/master/Symfony/Component/HttpKernel/HttpCache/HttpCache.html
Listing Symfony\Component\HttpFoundation\Response; 1 use 22-3 2 3 $app->get('/', function() { 4 return new Response('Foo', 200, array( 5 'Cache-Control' => 's-maxage=5', 6 )); 7 });
Listing 22-4
If you want Silex to trust the X-Forwarded-For* headers from your reverse proxy, you will need to run your application like this:
Listing Symfony\Component\HttpFoundation\Request; 1 use 2 22-5 3 Request::trustProxyData(); 4 $app->run();
Listing 22-6
This provider allows you to use the Symfony2 reverse proxy natively with Silex applications by using the http_cache service:
Listing 22-8
Listing 22-10
EOF , 200, array( 'Cache-Control' => 's-maxage=20', 'Surrogate-Control' => 'content="ESI/1.0"', )); }); $app->get('/included', function() { return new Response('Foo', 200, array( 'Cache-Control' => 's-maxage=5', )); }); $app['http_cache']->run();
To help you debug caching issues, set your application debug to true. Symfony automatically adds a X-Symfony-Cache header to each response with useful information about cache hits and misses. If you are not using the Symfony Session provider, you might want to set the PHP session.cache_limiter setting to an empty value to avoid the default PHP behavior. Finally, check that your Web server does not override your caching strategy.
3. http://symfony.com/doc/current/book/http_cache.html
Chapter 23
SecurityServiceProvider
The SecurityServiceProvider manages authentication and authorization for your applications.
Parameters
n/a
Services
security: The main entry point for the security provider. Use it to get the current user token. security.authentication_manager: An instance of AuthenticationProviderManager1, responsible for authentication. security.access_manager: An instance of AccessDecisionManager2, responsible for authorization. security.session_strategy: Define the session strategy used for authentication (default to a migration strategy). security.user_checker: Checks user flags after authentication. security.last_error: Returns the last authentication errors when given a Request object. security.encoder_factory: Defines the encoding strategies for user passwords (default to use a digest algorithm for all users).
The service provider defines many other services that are used internally but rarely need to be customized.
1. http://api.symfony.com/master/Symfony/Component/Security/Core/Authentication/AuthenticationProviderManager.html 2. http://api.symfony.com/master/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.html
Registering
Listing Listing 1 23-1 23-2
$app->register(new Silex\Provider\SecurityServiceProvider());
The Symfony Security Component comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency to your composer.json file:
Listing 23-3
Usage
The Symfony Security component is powerful. To learn more about it, read the Symfony2 Security documentation3.
When a security configuration does not behave as expected, enable logging (with the Monolog extension for instance) as the Security Component logs a lot of interesting information about what it does and why.
$token = $app['security']->getToken();
If there is no information about the user, the token is null. If the user is known, you can get it with a call to getUser():
The user can be a string, and object with a __toString() method, or an instance of UserInterface4.
3. http://symfony.com/doc/2.1/book/security.html 4. http://api.symfony.com/master/Symfony/Component/Security/Core/User/UserInterface.html
Listing 1 $app['security.firewalls'] = array( 23-8 2 'admin' => array( 3 'pattern' => '^/admin', 4 'http' => true, 5 'users' => array( 6 // raw password is foo 7 'admin' => array('ROLE_ADMIN', 8 '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='), 9 ), 10 ), );
Listing 23-9
The pattern is a regular expression (it can also be a RequestMatcher5 instance); the http setting tells the security layer to use HTTP basic authentication and the users entry defines valid users. Each user is defined with the following information: The role or an array of roles for the user (roles are strings beginning with ROLE_ and ending with anything you want); The user encoded password.
All users must at least have one role associated with them.
The default configuration of the extension enforces encoded passwords. To generate a valid encoded password from a raw password, use the security.encoder_factory service:
Listing // find the encoder for a UserInterface instance 23-10 $encoder = $app['security.encoder_factory']->getEncoder($user);
1 2 3 4 5
Listing 23-11
When the user is authenticated, the user stored in the token is an instance of User6
Listing 23-12
Listing 23-13
5. http://api.symfony.com/master/Symfony/Component/HttpFoundation/RequestMatcher.html 6. http://api.symfony.com/master/Symfony/Component/Security/Core/User/User.html
1 $app['security.firewalls'] = array( 2 'admin' => array( 3 'pattern' => '^/admin/', 4 'form' => array('login_path' => '/login', 'check_path' => '/admin/login_check'), 5 'users' => array( 6 'admin' => array('ROLE_ADMIN', 7 '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='), 8 ), 9 ), );
Always keep in mind the following two golden rules: The login_path path must always be defined outside the secured area (or if it is in the secured area, the anonymous authentication mechanism must be enabled -- see below); The check_path path must always be defined inside the secured area. For the login form to work, create a controller like the following:
Listing Listing 1 23-14 23-15
use Symfony\Component\HttpFoundation\Request; 2 3 $app->get('/login', function(Request $request) use ($app) { 4 return $app['twig']->render('login.html', array( 5 'error' => $app['security.last_error']($request), 6 'last_username' => $app['session']->get('_security.last_username'), 7 )); 8 });
The error and last_username variables contain the last authentication error and the last username entered by the user in case of an authentication error. Create the associated template:
Listing Listing 1 23-16 23-17
<form action="{{ path('admin_login_check') }}" method="post"> 2 {{ error }} 3 <input type="text" name="_username" value="{{ last_username }}" /> 4 <input type="password" name="_password" value="" /> 5 <input type="submit" /> 6 </form>
The admin_login_check route is automatically defined by Silex and its name is derived from the check_path value (all / are replaced with _ and the leading / is stripped).
It's also useful when you want to secure all URLs except the login form:
Listing 1 $app['security.firewalls'] = array( 23-18 2 'login' => array( 3 'pattern' => '^/login$', 4 ), 5 'secured' => array( 6 'pattern' => '^.*$', 7 'form' => array('login_path' => '/login', 'check_path' => '/login_check'), 8 'users' => array( 9 'admin' => array('ROLE_ADMIN', 10 '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='), 11 ), 12 ), );
Listing 23-19
The order of the firewall configurations is significant as the first one to match wins. The above configuration first ensures that the /login URL is not secured (no authentication settings), and then it secures all other URLs.
Adding a Logout
When using a form for authentication, you can let users log out if you add the logout setting:
Listing 1 $app['security.firewalls'] = array( 23-20 2 'secured' => array( 3 'form' => array('login_path' => '/login', 'check_path' => '/admin/login_check'), 4 'logout' => array('logout_path' => '/logout'), 5 6 // ... 7 ), 8 );
Listing 23-21
A route is automatically generated, based on the configured path (all / are replaced with _ and the leading / is stripped):
Listing 23-23
Listing 1 $app['security.firewalls'] = array( 23-24 2 'unsecured' => array( 3 'anonymous' => true, 4 5 // ...
Listing 23-25
6 7 );
),
When enabling the anonymous setting, a user will always be accessible from the security context; if the user is not authenticated, it returns the anon. string.
if ($app['security']->isGranted('ROLE_ADMIN') { 2 // ... 3 }
You can check is a user is "fully authenticated" (not an anonymous user for instance) with the special IS_AUTHENTICATED_FULLY role:
{% if is_granted('IS_AUTHENTICATED_FULLY') %} 2 <a href="{{ path('logout') }}">Logout</a> 3 {% else %} 4 <a href="{{ path('login') }}">Login</a> 5 {% endif %}
isGranted() throws an exception when no authentication information is available (which is the case on non-secured area).
Impersonating a User
If you want to be able to switch to another user (without knowing the user credentials), enable the switch_user authentication strategy:
Listing 1 $app['security.firewalls'] = array( 23-32 2 'unsecured' => array( 3 'switch_user' => array('parameter' => '_switch_user', 'role' => 4 'ROLE_ALLOWED_TO_SWITCH'), 5 6 // ... 7 ), );
Listing 23-33
Switching to another user is now a matter of adding the _switch_user query parameter to any URL when logged in as a user who has the ROLE_ALLOWED_TO_SWITCH role:
Listing 23-35
You can check that you are impersonating a user by checking the special ROLE_PREVIOUS_ADMIN. This is useful for instance to allow the user to switch back to his primary account:
Listing 1 {% if is_granted('ROLE_PREVIOUS_ADMIN') %} 23-36 2 You are an admin but you've switched to another user, 3 <a href="?_switch_user=_exit"> exit</a> the switch. 4 {% endif %}
Listing 23-37
Listing 23-39
With this configuration, all users with the ROLE_ADMIN role also automatically have the ROLE_USER and ROLE_ALLOWED_TO_SWITCH roles.
Listing 23-41
With the above configuration, users must have the ROLE_ADMIN to access the /admin section of the website, and ROLE_USER for everything else. Furthermore, the admin section can only be accessible via HTTPS (if that's not the case, the user will be automatically redirected).
The first argument can also be a RequestMatcher7 instance.
Here is a simple example of a user provider, where Doctrine DBAL is used to store the users:
Listing Listing 1 23-44 23-45
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
class UserProvider implements UserProviderInterface { private $conn; public function __construct(Connection $conn) { $this->conn = $conn; } public function loadUserByUsername($username) { $stmt = $this->conn->executeQuery('SELECT * FROM users WHERE username = ?', array(strtolower($username))); if (!$user = $stmt->fetch()) { throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); } return new User($user['username'], $user['password'], explode(',', $user['roles']), true, true, true, true); } public function refreshUser(UserInterface $user)
7. http://api.symfony.com/master/Symfony/Component/HttpFoundation/RequestMatcher.html 8. http://api.symfony.com/master/Symfony/Component/Security/Core/User/UserProviderInterface.html
31 { 32 if (!$user instanceof User) { 33 throw new UnsupportedUserException(sprintf('Instances of "%s" are not 34 supported.', get_class($user))); 35 } 36 37 return $this->loadUserByUsername($user->getUsername()); 38 } 39 40 public function supportsClass($class) { return $class === 'Symfony\Component\Security\Core\User\User'; } }
In this example, instances of the default User class are created for the users, but you can define your own class; the only requirement is that the class must implement UserInterface9 And here is the code that you can use to create the database schema and some sample users:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Listing Doctrine\DBAL\Schema\Table; use 23-46
Listing 23-47
$schema = $app['db']->getSchemaManager(); if (!$schema->tablesExist('users')) { $users = new Table('users'); $users->addColumn('id', 'integer', array('unsigned' => $users->setPrimaryKey(array('id')); $users->addColumn('username', 'string', array('length' $users->addUniqueIndex(array('username')); $users->addColumn('password', 'string', array('length' $users->addColumn('roles', 'string', array('length' => $schema->createTable($users);
$app['db']->executeQuery('INSERT INTO users (username, password, roles) VALUES ("fabien", "5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==", "ROLE_USER")'); $app['db']->executeQuery('INSERT INTO users (username, password, roles) VALUES ("admin", "5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==", "ROLE_ADMIN")'); }
If you are using the Doctrine ORM, the Symfony bridge for Doctrine provides a user provider class that is able to load users from your entities.
9. http://api.symfony.com/master/Symfony/Component/Security/Core/User/UserInterface.html
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
$app['security.authentication_listener.factory.wsse'] = $app->protect(function ($name, $options) use ($app) { // define the authentication provider object $app['security.authentication_provider.'.$name.'.wsse'] = $app->share(function () use ($app) { return new WsseProvider($app['security.user_provider.default'], __DIR__.'/security_cache'); });
// define the authentication listener object $app['security.authentication_listener.'.$name.'.wsse'] = $app->share(function () use ($app) { return new WsseListener($app['security'], $app['security.authentication_manager']); });
return array( // the authentication provider id 'security.authentication_provider.'.$name.'.wsse', // the authentication listener id 'security.authentication_listener.'.$name.'.wsse', // the entry point id null, // the position of the listener in the stack 'pre_auth' ); });
You can now use it in your configuration like any other built-in authentication provider:
Listing Listing 1 23-50 23-51
$app->register(new Silex\Provider\SecurityServiceProvider(), array( 2 'security.firewalls' => array( 3 'default' => array( 4 'wsse' => true, 5 6 // ... 7 ), 8 ), 9 ));
Instead of true, you can also define an array of options that customize the behavior of your authentication factory; it will be passed as the second argument of your authentication factory (see above). This example uses the authentication provider classes as described in the Symfony cookbook10.
10. http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html
Traits
Silex\Application\SecurityTrait adds the following shortcuts: user: Returns the current user. encodePassword: Encode a given password.
Listing 1 $user = $app->user(); 23-52 2 3 $encoded = $app->encodePassword($user, 'foo');
Listing 23-53
Silex\Route\SecurityTrait adds the following methods to the controllers: secure: Secures a controller for the given roles.
Listing 1 $app->get('/', function () { 23-54 2 // do something but only for admins 3 })->secure('ROLE_ADMIN');
Listing 23-55
Chapter 24
Webserver Configuration
Apache
If you are using Apache you can use a .htaccess file for this:
<IfModule mod_rewrite.c> 2 Options -MultiViews 3 4 RewriteEngine On 5 #RewriteBase /path/to/app 6 RewriteCond %{REQUEST_FILENAME} !-f 7 RewriteRule ^ index.php [L] 8 </IfModule>
If your site is not at the webroot level you will have to uncomment the RewriteBase statement and adjust the path to point to your directory, relative from the webroot.
Alternatively, if you use Apache 2.2.16 or higher, you can use the FallbackResource directive1 so make your .htaccess even easier:
Listing Listing 1 24-3 24-4
FallbackResource /index.php
1. http://www.adayinthelifeof.nl/2012/01/21/apaches-fallbackresource-your-new-htaccess-command/
If your site is not at the webroot level you will have to adjust the path to point to your directory, relative from the webroot.
nginx
If you are using nginx, configure your vhost to forward non-existent resources to index.php:
Listing 1 server { 24-5 2 index index.php 3 4 location / { 5 try_files $uri $uri/ /index.php; 6 } 7 8 location ~ index\.php$ { 9 fastcgi_pass unix:/var/run/php5-fpm.sock; 10 fastcgi_index index.php; 11 include fastcgi_params; 12 } 13 }
Listing 24-6
IIS
If you are using the Internet Information Services from Windows, you can use this sample web.config file:
Listing 1 <?xml version="1.0"?> 24-7 2 <configuration> 3 <system.webServer> 4 <defaultDocument> 5 <files> 6 <clear /> 7 <add value="index.php" /> 8 </files> 9 </defaultDocument> 10 <rewrite> 11 <rules> 12 <rule name="Silex Front Controller" stopProcessing="true"> 13 <match url="^(.*)$" ignoreCase="false" /> 14 <conditions logicalGrouping="MatchAll"> 15 <add input="{REQUEST_FILENAME}" matchType="IsFile" 16 ignoreCase="false" negate="true" /> 17 </conditions> 18 <action type="Rewrite" url="index.php" appendQueryString="true" /> 19 </rule> 20 </rules> 21 </rewrite> 22 </system.webServer> </configuration>
Listing 24-8
Lighttpd
If you are using lighttpd, use this sample simple-vhost as a starting point:
server.document-root = "/path/to/app" 2 3 url.rewrite-once = ( 4 # configure some static files 5 "^/assets/.+" => "$0", 6 "^/favicon\.ico$" => "$0", 7 8 "^(/[^\?]*)(\?.*)?" => "/index.php$1$2" 9 )
PHP 5.4
PHP 5.4 ships with a built-in webserver for development. This server allows you to run silex without any configuration. However, in order to serve static files, you'll have to make sure your front controller returns false in that case:
Listing Listing 1 24-11 24-12
// web/index.php
$filename = __DIR__.preg_replace('#(\?.*)$#', '', $_SERVER['REQUEST_URI']); if (php_sapi_name() === 'cli-server' && is_file($filename)) { return false; } $app = require __DIR__.'/../src/app.php'; $app->run();
2 3 4 5 6 7 8 9
Assuming your front controller is at web/index.php, you can start the server from the command-line with this command:
Listing Listing 1 24-13 24-14
Chapter 25
Changelog
2012-07-15: removed the monolog.configure service. Use the extend method instead: Before:
Listing 1 $app['monolog.configure'] = $app->protect(function ($monolog) use 25-1 2 ($app) { 3 // do something });
Listing 25-2
After:
Listing 1 $app['monolog'] = $app->share($app->extend('monolog', 25-3 2 function($monolog, $app) { 3 // do something 4 5 return $monolog; }));
Listing 25-4
2012-06-17: ControllerCollection now takes a required route instance as a constructor argument. Before:
Listing 1 $controllers = new ControllerCollection(); 25-5
Listing 25-6
After:
2012-06-17: added application traits for PHP 5.4 2012-06-16: renamed request.default_locale to locale 2012-06-16: Removed the translator.loader service. See documentation for how to use XLIFF or YAML-based translation files. 2012-06-15: removed the twig.configure service. Use the extend method instead: Before:
Listing Listing 1 25-925-10
After:
Listing Listing 1 25-11 25-12
2012-06-13: Added a route before middleware 2012-06-13: Renamed the route middleware to before 2012-06-13: Added an extension for the Symfony Security component 2012-05-31: Made the BrowserKit, CssSelector, DomCrawler, Finder and Process components optional dependencies. Projects that depend on them (e.g. through functional tests) should add those dependencies to their composer.json. 2012-05-26: added boot() to ServiceProviderInterface. 2012-05-26: Removed SymfonyBridgesServiceProvider. It is now implicit by checking the existence of the bridge. 2012-05-26: Removed the translator.messages parameter (use translator.domains instead). 2012-05-24: Removed the autoloader service (use composer instead). The *.class_path settings on all the built-in providers have also been removed in favor of Composer. 2012-05-21: Changed error() to allow handling specific exceptions. 2012-05-20: Added a way to define settings on a controller collection. 2012-05-20: The Request instance is not available anymore from the Application after it has been handled. 2012-04-01: Added finish filters. 2012-03-20: Added json helper:
Listing 25-14
2012-03-11: Added route middlewares. 2012-03-02: Switched to use Composer for dependency management. 2012-02-27: Updated to Symfony 2.1 session handling. 2012-01-02: Introduced support for streaming responses. 2011-09-22: ExtensionInterface has been renamed to ServiceProviderInterface. All built-in extensions have been renamed accordingly (for instance, Silex\Extension\TwigExtension has been renamed to Silex\Provider\TwigServiceProvider). 2011-09-22: The way reusable applications work has changed. The mount() method now takes an instance of ControllerCollection instead of an Application one. Before:
Listing = new Application(); 1 $app 25-15 2 $app->get('/bar', function() { return 'foo'; }); 3 4 return $app;
Listing 25-16
After:
Listing = new ControllerCollection(); 1 $app 25-17 2 $app->get('/bar', function() { return 'foo'; }); 3 4 return $app;
Listing 25-18
2011-08-08: The controller method configuration is now done on the Controller itself Before:
Listing 1 $app->match('/', function () { echo 'foo'; }, 'GET|POST'); 25-19
Listing 25-20
After:
Listing 1 $app->match('/', function () { echo 'foo'; })->method('GET|POST'); 25-21
Listing 25-22
Chapter 26
Phar File
Using the Silex phar file is deprecated. You should use Composer instead to install Silex and its dependencies or download one of the archives.
Installing
Installing Silex is as easy as downloading the phar1 and storing it somewhere on the disk. Then, require it in your script:
Listing Listing 1 26-1 26-2
<?php require_once __DIR__.'/silex.phar'; $app = new Silex\Application(); $app->get('/hello/{name}', function ($name) use ($app) { return 'Hello '.$app->escape($name); }); $app->run();
2 3 4 5 6 7 8 9 10 11
Console
Silex includes a lightweight console for updating to the latest version. To find out which version of Silex you are using, invoke silex.phar on the command-line with version as an argument:
1. http://silex.sensiolabs.org/get/silex.phar
Listing 1 $ php silex.phar version 26-3 2 Silex version 0a243d3 2011-04-17 14:49:31 +0200
Listing 26-4
To check that your are using the latest version, run the check command:
Listing 26-6
Listing 26-8
This will automatically download a new silex.phar from silex.sensiolabs.org and replace the existing one.
Pitfalls
There are some things that can go wrong. Here we will try and outline the most frequent ones.
PHP configuration
Certain PHP distributions have restrictive default Phar settings. Setting the following may help.
Listing 1 detect_unicode = Off 26-9 2 phar.readonly = Off 3 phar.require_hash = Off
Listing 26-10
Listing 26-12
Ubuntu's PHP ships with Suhosin, so if you are using Ubuntu, you will need this change.
Phar-Stub bug
Some PHP installations have a bug that throws a PharException when trying to include the Phar. It will also tell you that Silex\Application could not be found. A workaround is using the following include line:
require_once 'phar://'.__DIR__.'/silex.phar/autoload.php';
zend_extension = /usr/lib/php5/20090626+lfs/ioncube_loader_lin_5.3.so