(Web) Developer / OSS enthusiast
Graduated from IUT, Aix-Marseille University and Blaise Pascal University.
Worked since 2009 in various companies. Currently employed by TEA — The Ebook Alternative.
Open-Source enthusiast & contributor:
https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project
Injection flaws, such as SQL, OS, XXE, and LDAP injection occur when untrusted data is sent to an interpreter as part of a command or query. The attacker’s hostile data can trick the interpreter into executing unintended commands or accessing data without proper authorization.
GET
/POST
parametersPATH_INFO
Unescaped user input causes the premature end of a SQL query, and allows a malicious query to be executed:
$query = 'SELECT * FROM foo WHERE bar = "' . $_GET['bar'] . '"';
// example.org?bar=nope" OR 1 = 1 --
Filesystem access combined to unvalidated user input allows attackers to access private files:
echo file_get_contents(__DIR__.'/'.$_GET['file']);
// example.org?file=../../private.conf
Unsafe input is dynamically executed:
exec('rm -rf web/upload/' . $_GET['file']);
// example.org?file=*; rm -rf /;
basename()
, realpath()
;htmlspecialchars()
(or better: use a template engine that escapes
everything by default);shell_escape_args()
;eval()
or exec()
functions.; Helps mitigate XSS by telling the browser not to expose the cookie to
; client side scripting such as JavaScript
session.cookie_httponly = 1
; Prevents session fixation by making sure that PHP only uses cookies for
; sessions and disallow session ID passing as a GET parameter
session.use_only_cookies = 1
; Better entropy source
; Evades insufficient entropy vulnerabilities
session.entropy_file = "/dev/urandom"
; Might help against brut-force attacks too!
session.entropy_length=32
; Smaller exploitation window for XSS/CSRF/Clickjacking...
session.cookie_lifetime = 0
; Ensures session cookies are only sent over secure connections
; (it requires a valid SSL certificate)
; Related to OWASP 2013-A6-Sensitive Data Exposure
session.cookie_secure = 1
htmlspecialchars()
;Defines the Content-Security-Policy
HTTP header
that allows you to create
a whitelist of sources of trusted content, and instructs the browser to
only execute or
render resources from those sources.
Content-Security-Policy: script-src 'self' https://apis.google.com
Restrictions on what authenticated users are allowed to do are not properly enforced. Attackers can exploit these flaws to access unauthorized functionality and/or data, such as access other users' accounts, view sensitive files, modify other users' data, change access rights, etc.
shell_exec
, …) Facebook CSRF worth USD 5000
Does Google Understand CSRF?
A few CSRF-like vulnerable examples.
Using a zero-byte image attack:
<img
src="https://bank.com/transfer.do?acct=BOB&amount=100"
height="0" width="0"
/>
Using a link, asking the victim to click on it:
<a href="http://bank.com/transfer.do?acct=BOB&amount=100">
View my Pictures!
</a>
A malicious page can issue a POST request to any domain:
<form method="POST" action="http://example.org/form">
<input type="text" name="name" value="Joe la frite">
<input type="text" name="message" value="Hello, World!">
<!-- ... -->
</form>
With a few lines of JavaScript:
$(document).ready(function() {
$('form').submit();
});
GET
method;The same-origin policy restricts how a document or script loaded from one origin can interact with a resource from another origin.
roave/security-advisories:dev-master
in your composer.json
;A framework helps you work better by structuring developments, and faster by reusing generic modules.
A framework facilitates long-term maintenance and scalability by complying with standard development rules.
Compliance with development standards also simplifies integrating and interfacing the application with the rest of the information system.
In other words, it works as a tool to make the development process easier and more productive.
Most of the time, a framework implements many kinds of design patterns.
Read more: Symfony explained to a Developer.
First of all:
Symfony is a reusable set of standalone, decoupled, and cohesive PHP components that solve common web development problems.
Then, based on these components:
Symfony is also a full-stack web framework.
Fabien Potencier, http://fabien.potencier.org/article/49/what-is-symfony2.
Symfony is built on powerful concepts:
It has been written by ~1679 developers.
Open Source, MIT licensed.
The Components implement common features needed to develop websites.
They are the foundation of the Symfony full-stack framework, but they can also be used standalone even if you don't use the framework as they don't have any mandatory dependencies.
There are ~50 components, including:
BrowserKit EventDispatcher OptionsResolver Templating
ClassLoader ExpressionLanguage Process Translation
Config Filesystem PropertyAccess VarDumper
Console Finder PropertyInfo Yaml
CssSelector Form Routing
Debug HttpFoundation Security
DependencyInjection HttpKernel Serializer
DomCrawler Intl Stopwatch
Say you want to play with YAML files, start by requiring the symfony/yaml
component into your composer.json
file:
{
"require": {
"symfony/yaml": "^3.0"
}
}
Install it by running php composer.phar install
, and use it:
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Yaml\Yaml;
$yaml = Yaml::parse('/path/to/file.yml');
http://symfony.com/doc/current/components/yaml/introduction.html
The Symfony Framework accomplishes two distinct tasks:
The goal of the framework is to integrate many independent tools in order to provide a consistent experience for the developer. Even the framework itself is a Symfony bundle (i.e. a plugin) that can be configured or replaced entirely.
Symfony provides a powerful set of tools for rapidly developing web applications without imposing on your application.
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();
// the HTTP verb
$request->getMethod();
// GET variables
$request->query->get('foo');
// POST variables
$request->request->get('bar');
// SERVER variables
$request->server->get('HTTP_HOST');
// retrieve an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
use Symfony\Component\HttpFoundation\Response;
$response = new Response();
$response->setContent(<<<HTML
<html>
<body>
<h1>Hello world!</h1>
</body>
</html>
HTML
);
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');
// prints the HTTP headers followed by the content
$response->send();
// index.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$path = $request->getPathInfo();
if (in_array($path, ['', '/'])) {
$response = new Response('Welcome to the homepage.');
} elseif ('/hello' === $path) {
$response = new Response('hello, World!');
} else {
$response = new Response('Page not found.', 404);
}
$response->send();
It's all about transforming a Request into a Response:
The routing system determines which PHP function should be executed based on information from the request and routing configuration you've created.
# app/config/routing.yml
hello:
path: /hello
defaults: { _controller: AppBundle:Main:hello }
The AppBundle:Main:hello
string is a short syntax that points to a
specific PHP method named helloAction()
inside a class called
MainController
.
This example uses YAML to define the routing configuration. Routing configuration can also be written in other formats such as XML or PHP.
In Symfony, a method in a controller is called an action. The convention is
to suffix each method with Action
.
Also, each controller should be suffixed with Controller
and placed in a
Controller
namespace.
// src/AppBundle/Controller/MainController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class MainController
{
public function helloAction()
{
return new Response('<h1>Hello, World!</h1>');
}
}
Recommended structure of a Symfony (3.x) project:
path/to/project/
app/
config/
Resources/
views/
bin/
console
src/
...
tests/
...
var/
cache/
logs/
sessions/
vendor/
...
web/
app.php
...
Each directory has its own purpose (and set of files):
app/
contains the application kernel, views, and the configuration;src/
contains your code and bundles;tests/
contains your tests;var/
contains files that change often (like in Unix systems);vendor/
contains your dependencies;web/
contains your front controllers and your assets.This is the central part of your application:
// app/AppKernel.php
use Symfony\Component\HttpKernel\Kernel;
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
// ...
];
if (in_array($this->getEnvironment(), ['dev', 'test'])) {
$bundles[] = // dev bundle;
}
return $bundles;
}
// ...
}
An application consists of a collection of "bundles" representing all of the features and capabilities of your application.
Each "bundle" can be customized via configuration files written in YAML
, XML
or PHP
.
By default, the main configuration file lives in the app/config/
directory and is called either config.yml
, config.xml
or config.php
depending on which format you prefer.
Symfony is all about configuring everything, and you can do pretty much everything you want. That's why people agreed on some conventions, but then again, a convention is just A way to do things, not THE way to do them.
# app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
framework:
secret: '%secret%'
router: { resource: '%kernel.root_dir%/config/routing.yml' }
# ...
# Twig Configuration
twig:
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
# ...
<!-- app/config/config.xml -->
<imports>
<import resource="parameters.yml"/>
<import resource="security.yml"/>
</imports>
<framework:config secret="%secret%">
<framework:router resource="%kernel.root_dir%/config/routing.xml"/>
<!-- ... -->
</framework:config>
<!-- Twig Configuration -->
<twig:config debug="%kernel.debug%" strict-variables="%kernel.debug%"/>
<!-- ... -->
$this->import('parameters.yml');
$this->import('security.yml');
$container->loadFromExtension('framework', [
'secret' => '%secret%',
'router' => [
'resource' => '%kernel.root_dir%/config/routing.php'
],
// ...
]);
// Twig Configuration
$container->loadFromExtension('twig', [
'debug' => '%kernel.debug%',
'strict_variables' => '%kernel.debug%',
]);
// ...
The main configuration MUST be written in YAML
:
# app/config/config.yml
# ...
twig:
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
The routing definition MUST be written in YAML
:
# app/config/routing.yml
hello:
path: /hello
defaults: { _controller: AppBundle:Main:hello }
The DI Container configuration MUST be written in YAML
:
services:
acme_demo.controllers.main:
class: AppBundle\Controller\MainController
An application can run in various environments. The different environments share the same PHP code, but use different configuration.
A Symfony project generally uses three environments: dev
, test
and prod
.
// web/app.php
// ...
$kernel = new AppKernel('prod', /* $debug = */ false);
The AppKernel
class is responsible for actually loading the configuration file
of your choice:
// app/AppKernel.php
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(
__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml'
);
}
A Bundle is a directory containing a set of files (PHP files, stylesheets, JavaScripts, images, ...) that implement a single feature (a blog, a forum, etc).
It should be reusable, so that you don't reinvent the wheel each time you need a common feature. In Symfony, (almost) everything lives inside a bundle.
In order to use a bundle in your application, you need to register it in the
AppKernel
, using the registerBundles()
method:
public function registerBundles()
{
$bundles = [
// ...
new My\AwesomeBundle\MyAwesomeBundle(),
];
// ...
}
Recommended structure for a bundle:
XXX/...
DemoBundle/
DemoBundle.php
Controller/
Resources/
config/
doc/
index.md
translations/
views/
public/
Tests/
LICENSE
The DemoBundle
class is mandatory, and both LICENSE
and
Resources/doc/index.md
files should be present.
The XXX
directory(ies) reflects the namespace structure of the bundle.
Type | Directory |
---|---|
Commands | Command/ |
Controllers | Controller/ |
Service Container Extensions | DependencyInjection/ |
Event Listeners/Subscribers | EventListener/ |
Configuration | Resources/config/ |
Web Resources | Resources/public/ |
Translation files | Resources/translations/ |
Templates | Resources/views/ |
Unit and Functional Tests | Tests/ |
A bundle has to extend the Symfony\Component\HttpKernel\Bundle\Bundle
class:
// src/Acme/MyFirstBundle/AcmeMyFirstBundle.php
namespace Acme\MyFirstBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeMyFirstBundle extends Bundle
{
}
Then, you can register your bundle:
// app/AppKernel.php
public function registerBundles()
{
$bundles = [
new Acme\MyFirstBundle\AcmeMyFirstBundle(),
];
return $bundles;
}
The web root directory is the home of all public and static files including images, stylesheets, and JavaScript files. It is also where each front controller lives:
// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
The front controller file (app.php
in this example) is the actual PHP file
that's executed when using a Symfony application and its job is to use a
Kernel class, AppKernel
, to bootstrap the application, for a given
environment.
Creating a page is a three-step process involving a route, a controller, and (optionally) a template.
Each project contains just a few main directories: web/
(web assets and the
front controllers), app/
(configuration), src/
(your bundles), and vendor/
(third-party code).
Each feature in Symfony (including the Symfony framework core) is organized into a bundle, which is a structured set of files for that feature.
The configuration for each bundle lives in the Resources/config
directory of the
bundle and can be specified in YAML
, XML
or PHP
.
The global application configuration lives in the app/config/
directory.
Each environment is accessible via a different front controller (e.g. app.php
and app_dev.php
) and loads a different configuration file.
A controller is a PHP function you create that takes information from the HTTP request and constructs and returns an HTTP response.
Every request handled by a Symfony project goes through the same lifecycle:
app.php
or
app_dev.php
) that bootstraps the application;_controller
parameter from the
route;Response
object;Response
object are sent back to the
client.# app/config/routing.yml
homepage:
path: /
defaults: { _controller: AppBundle:Hello:index }
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction()
{
return new Response('Home, Sweet Home!');
}
}
Every route must have a _controller
parameter, which dictates which controller
should be executed when that route is matched.
This parameter uses a simple string pattern called the logical controller name.
The pattern has three parts, each separated by a colon: bundle:controller:action
.
For example, a _controller
value of AcmeBlogBundle:Blog:show
means:
AcmeBlogBundle
;BlogController
;showAction
.Notice that Symfony adds the string Controller
to the class name (Blog
=>
BlogController
) and Action
to the method name (show
=> showAction
).
# src/AppBundle/Resources/config/routing.yml
app.hello_hello:
path: /hello/{name}
defaults: { _controller: AppBundle:Hello:hello }
requirements:
_method: GET
// src/AppBundle/Controller/HelloController.php
class HelloController
{
// ...
public function helloAction($name)
{
return new Response(sprintf('Home, Sweet %s!', $name));
}
}
For convenience, you can also have Symfony pass you the Request object as an argument to your controller:
use Symfony\Component\HttpFoundation\Request;
class HelloController
{
// ...
public function updateAction(Request $request)
{
// do something useful with $request
}
}
This is useful when you are working with forms.
Symfony comes with a base Controller
class that assists with some of the most
common controller tasks and gives your controller class access to any resource
it might need:
use Symfony\Bundle\FrameworkBundle\Controller\Controller
class HelloController extends Controller
{
// ...
}
$this->redirect($this->generateUrl('homepage')); // see also: redirectToRoute
return $this->render('hello/hello.html.twig', [
'name' => $name
]);
The only requirement for a controller is to return a Response
object.
Create a simple Response
with a 200
status code:
use Symfony\Component\HttpFoundation\Response;
$response = new Response('Hello, ' . $name, 200);
Create a JSON response with a 200
status code:
$response = new Response(json_encode(['name' => $name]));
$response->headers->set('Content-Type', 'application/json');
Or:
use Symfony\Component\HttpFoundation\JsonResponse;
$response = new JsonResponse(['name' => $name]);
The Symfony router lets you define URLs that you map to different areas of your application.
A route is a map from a URL path to a controller. Each route is named, and
maps a path
to a _controller
:
# app/config/routing.yml
homepage:
path: /
defaults: { _controller: 'AppBundle:Hello:index' }
This route matches the homepage (/
) and maps it to the
AppBundle:Hello:index
controller.
blog:
path: /blog/{page}
defaults: { _controller: 'AcmeBlogBundle:Blog:index' }
The path will match anything that looks like /blog/*
.
Even better, the value matching the {page}
placeholder will be available
inside your controller.
/blog
will not match.
blog:
path: /blog/{page}
defaults:
_controller: 'AcmeBlogBundle:Blog:index'
page: 1
By adding page
to the defaults key, {page}
is no longer required.
/blog
will match this route and the value of the page
parameter will be
set to 1
. /blog/2
will also match, giving the page
parameter a value of 2
.
blog:
path: /blog/{page}
defaults:
_controller: 'AcmeBlogBundle:Blog:index'
page: 1
requirements:
page: \d+
The \d+
requirement is a regular expression that says that the value of
the {page}
parameter must be a digit (i.e. a number).
# src/AppBundle/Resources/config/routing.yml
app.hello_hello:
path: /hello/{name}
defaults: { _controller: 'AppBundle:Hello:hello' }
methods: [ GET ]
# methods: [ GET, POST ]
All routes are loaded via a single configuration file, most of the time it will
be app/config/routing.yml
.
In order to respect the "bundle" principle, the routing configuration should be located in the bundle itself, and you should just require it:
# app/config/routing.yml
app:
resource: '@AppBundle/Resources/config/routing.yml'
# app/config/routing.yml
app:
resource: '@AppBundle/Resources/config/routing.yml'
prefix: /demo
The string /demo
now be prepended to the path of each route loaded from
the new routing resource.
The Router
is able to generate both relative and absolute URLs.
$router = $this->get('router');
$router->generate('app.hello_hello', [ 'name' => 'will' ]);
// /hello/will
$router->generate('app.hello_hello', [ 'name' => 'will' ], true);
// http://example.com/hello/will
$router->generate('app.hello_hello', [
'name' => 'will', 'some' => 'thing'
]);
// /hello/will?some=thing
Fast, Secure, Flexible.
<ul id="navigation">
<?php foreach ($navigation as $item): ?>
<li>
<a href="<?php echo $item->getHref() ?>">
<?php echo $item->getCaption() ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<ul id="navigation">
{% for item in navigation %}
<li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %}
</ul>
{{ ... }}
: prints a variable or the result of an expression;{% ... %}
: controls the logic of the template; it is used to execute for
loops and if statements, for example;{# ... #}
: comments.{# array('name' => 'Fabien') #}
{{ name }}
{# array('user' => array('name' => 'Fabien')) #}
{{ user.name }}
{# force array lookup #}
{{ user['name'] }}
{# array('user' => new User('Fabien')) #}
{{ user.name }}
{{ user.getName }}
{# force method name lookup #}
{{ user.name() }}
{{ user.getName() }}
{# pass arguments to a method #}
{{ user.date('Y-m-d') }}
{% if user.isSuperAdmin() %}
...
{% elseif user.isMember() %}
...
{% else %}
...
{% endif %}
<ul>
{% for user in users if user.active %}
<li>{{ user.username }}</li>
{% else %}
<li>No users found</li>
{% endfor %}
</ul>
Filters are used to modify Twig variables.
You can use inline filters by using the |
symbol:
{{ 'hello'|upper }}
But you can also use the block syntax:
{% filter upper %}
hello
{% endfilter %}
Filters can be parametrized:
{{ post.createdAt|date('Y-m-d') }}
The include
tag is useful to include a template and return the rendered
content of that template into the current one:
{% include 'sidebar.html' %}
Given the following template:
{% for user in users %}
{% include "render_user.html" %}
{% endfor %}
with render_user.html
:
<p>{{ user.username }}</p>
<p>William D.</p>
<p>Julien M.</p>
Let's define a base template, base.html
, which defines a simple HTML skeleton:
{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Test Application{% endblock %}</title>
</head>
<body>
<div id="sidebar">
{% block sidebar %}
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
{% endblock %}
</div>
<div id="content">
{% block body %}{% endblock %}
</div>
</body>
</html>
The key to template inheritance is the {% extends %}
tag.
A child template might look like this:
{# app/Resources/views/Blog/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}My cool blog posts{% endblock %}
{% block body %}
{% for entry in blog_entries %}
<h2>{{ entry.title }}</h2>
<p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
If you need to get the content of a block from the parent template, you can
use the {{ parent() }}
function.
By default, templates can live in two different locations:
app/Resources/views/
: The applications views directory can contain
application-wide base templates (i.e. your application's layouts),
templates specific to your app as well as templates that override bundle
templates;path/to/bundle/Resources/views/
: Each (public) bundle houses its templates in its
Resources/views
directory (and subdirectories).Symfony uses a bundle:controller:template
string syntax for templates.
You can skip the controller
string: bundle::template
. The template
file would live in Resources/views/
.
You can also skip the bundle
string. It refers to an application-wide base
template or layout. This means that the template is not located in any bundle,
but instead in the root app/Resources/views/
directory.
AcmeBlogBundle:Blog:index.html.twig
AcmeBlogBundle
: (bundle) the template lives inside the AcmeBlogBundle
(e.g.
src/Acme/BlogBundle
);Blog
: (controller) indicates that the template lives inside the Blog
subdirectory of Resources/views
;index.html.twig
: (template) the actual name of the file is index.html.twig
.Assuming that the AcmeBlogBundle
lives at src/Acme/BlogBundle
, the final
path to the layout would be:
src/Acme/BlogBundle/Resources/views/Blog/index.html.twig
Once you use a third-party bundle, you'll likely need to override and customize one or more of its templates.
When the FooBarBundle:Bar:index.html.twig
is rendered, Symfony actually
looks in two different locations for the template:
app/Resources/FooBarBundle/views/Bar/index.html.twig
;src/Foo/BarBundle/Resources/views/Bar/index.html.twig
.In order to override the bundle template, copy the index.html.twig
template
from the bundle to: app/Resources/FooBarBundle/views/Bar/index.html.twig
.
The core TwigBundle contains a number of different templates that can be
overridden by copying each from the Resources/views/
directory of the
TwigBundle to the app/Resources/TwigBundle/views/
directory.
public function listAction()
{
// ...
return $this->render('blog/index.html.twig', [
'posts' => $posts,
]);
}
$engine = $this->container->get('templating');
$content = $engine->render('blog/index.html.twig', [
'posts' => $posts,
]);
return new Response($content);
Assuming the following routing definition:
homepage:
path: /
defaults: { _controller: AppBundle:Hello:index }
acme_blog.post_show:
path: /posts/{slug}
defaults: { _controller: AcmeBlogBundle:Post:show }
You can create a relative URL using path()
:
<a href="{{ path('homepage') }}">Home</a>
You can create an absolute URL using url()
:
<a href="{{ url('homepage') }}">Home</a>
The second argument is used to pass parameters:
<a href="{{ path('acme_blog.post_show', {'slug': 'my-super-slug'}) }}">
<script src={{ asset('js/script.js') }}></script>
<link href="{{ asset('css/style.css') }}" rel="stylesheet">
<img src="{{ asset('images/logo.png') }}" alt="Symfony!" />
Cache busting is the process of forcing browsers or proxy servers to update their cache, for instance, JavaScript and CSS files or images.
# app/config/config.yml
framework:
# ...
templating: { engines: ['twig'], assets_version: v2 }
The asset_version
parameter is used to bust the cache on assets by globally
adding a query parameter to all rendered asset paths:
/images/logo.png?v2
The FOSJsRoutingBundle allows you to expose your routing in your JavaScript code. That means you'll be able to generate URL with given parameters like you can do with the Router component provided by Symfony.
# app/config/routing.yml
my_route_to_expose:
path: /foo/{id}/bar
defaults: { _controller: FooBarBundle:Foo:bar }
options:
expose: true
According to the routing definition above, you can write the following JavaScript code to generate URLs:
Routing.generate('my_route_to_expose', { id: 10 });
// /foo/10/bar
Routing.generate('my_route_to_expose', { id: 10 }, true);
// http://example.org/foo/10/bar
app.security
: the security context;app.user
: the current user object;app.request
: the request object;app.session
: the session object;app.environment
: the current environment (dev
, prod
, etc);app.debug
: true
if in debug mode. false
otherwise.A Service is a generic term for any PHP object that performs a specific task.
A service is usually used globally, such as a database connection object or an object that delivers email messages.
In Symfony, services are often configured and retrieved from the service container.
An application that has many decoupled services is said to follow a Service-Oriented Architecture (SOA).
A Service Container, also known as a Dependency Injection Container (DIC), is a special object that manages the instantiation of services inside an application.
The service container takes care of lazily instantiating and injecting dependent services.
class Foo
{
private $bar;
private $debug;
public function __construct(Bar $bar = null, $debug = false)
{
$this->bar = $bar;
$this->debug = $debug;
}
}
The service definition for the class described above is:
services:
foo:
class: My\Bundle\Foo
This service is now available in the container, and you can access it by asking the service from the container:
$foo = $this->container->get('foo');
The service definition described before is not flexible enough. For instance,
$debug
argument is never configured.
Parameters make defining services more organized and flexible:
services:
foo:
class: My\Bundle\Foo
arguments:
- ~ # null
- '%kernel.debug%'
In the definition above, kernel.debug
is a parameter defined by the framework
itself. The foo
service is now parametrized.
As you may noticed, the Foo
class takes an instance of Bar
as first
argument. You can inject this instance in your foo
service by
referencing the bar
service:
services:
bar:
class: My\Bundle\Bar
foo:
class: My\Bundle\Foo
arguments:
- '@bar'
- '%kernel.debug%'
calls:
- [setBar, ['@bar']]
imports
Way# app/config/config.yml
imports:
- { resource: "@AcmeDemoBundle/Resources/config/services.yml" }
A service container extension is a PHP class to accomplish two things:
An extension class should live in the DependencyInjection
directory of your
bundle and its name should be constructed by replacing the Bundle
suffix of
the Bundle class name with Extension
.
// Acme/DemoBundle/DependencyInjection/AcmeDemoExtension.php
namespace Acme\DemoBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class AcmeDemoExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(
__DIR__ . '/../Resources/config'
));
$loader->load('services.yml');
}
}
The presence of the previous class means that you can now define an
acme_demo
configuration namespace in any configuration file:
# app/config/config.yml
acme_demo: ~
Take the following configuration:
acme_demo:
foo: fooValue
bar: barValue
The array passed to your load()
method will look like this:
array(
array(
'foo' => 'fooValue',
'bar' => 'barValue',
)
)
The $configs
argument is an array of arrays, not just a single flat array
of the configuration values.
It's your job to decide how these configurations should be merged together.
You might, for example, have later values override previous values or somehow merge them together:
public function load(array $configs, ContainerBuilder $container)
{
$config = array();
foreach ($configs as $subConfig) {
$config = array_merge($config, $subConfig);
}
// ... now use the flat $config array
}
// src/Acme/DemoBundle/DependencyInjection/Configuration.php
namespace Acme\DemoBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('acme_demo');
$rootNode
->children()
->scalarNode('my_type')->defaultValue('bar')->end()
->end();
return $treeBuilder;
}
}
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
// ...
}
The processConfiguration()
method uses the configuration tree you've defined
in the Configuration
class to validate, normalize and merge all of
the configuration arrays together.
How to Create Friendly Configuration for a Bundle: https://symfony.com/doc/master/bundles/configuration.html.
In the service container, a tag implies that the service is meant to be used for a specific purpose.
services:
my_bundle.twig.foo:
class: My\Bundle\Twig\FooExtension
tags:
- { name: twig.extension }
Twig finds all services tagged with twig.extension
and automatically registers
them as extensions.
$ php bin/console debug:container
$ php bin/console debug:container foo
$ php bin/console
You can get help information:
$ php bin/console help cmd
$ php bin/console cmd --help
$ php bin/console cmd -h
You can get more verbose messages:
$ php bin/console cmd --verbose
$ php bin/console cmd -v [-vv] [-vvv]
You can suppress output:
$ php bin/console cmd --quiet
$ php bin/console cmd -q
assets
assets:install Installs bundles web assets under a public
cache
cache:clear Clears the cache
cache:warmup Warms up an empty cache
config
config:dump-reference Dumps default configuration for an extension
debug
debug:container Displays current services for an application
debug:event-dispatcher Displays configured listeners for an application
debug:router Displays current routes for an application
web directory
debug:twig Shows a list of twig functions, filters, […]
router
router:match Helps debug routes by simulating a path info match
server
server:run Runs PHP built-in web server
lint
lint:twig Lints a template and outputs encountered
errors
Create a Command
directory inside your bundle and create a php file suffixed
with Command.php
for each command that you want to provide:
// src/AppBundle/Command/GreetCommand.php
namespace AppBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class GreetCommand extends ContainerAwareCommand
{
protected function configure()
{
$this->setName('demo:greet');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// code ...
}
}
Arguments are the strings, separated by spaces, that come after the command name itself. They are ordered, and can be optional or required.
protected function configure()
{
$this
// ...
->addArgument(
'name', InputArgument::REQUIRED, 'Who do you want to greet?'
)
->addArgument(
'last_name', InputArgument::OPTIONAL, 'Your last name?'
);
}
// php bin/console demo:greet Kévin Gomez
$input->getArgument('last_name');
Unlike arguments, options are not ordered, always optional, and can be setup to accept a value or simply as a boolean flag without a value.
protected function configure()
{
$this
// ...
->addOption(
'yell', null, InputOption::VALUE_NONE,
'If set, the task will yell in uppercase letters'
);
}
// php bin/console demo:greet --yell
if ($input->getOption('yell')) {
// ...
}
protected function configure()
{
$this
// ...
->addOption(
'iterations', null, InputOption::VALUE_REQUIRED,
'How many times should the message be printed?',
1
);
}
// php bin/console demo:greet --iterations=10
for ($i = 0; $i < $input->getOption('iterations'); $i++) {
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$translator = $this->getContainer()->get('translator');
// ...
}
$command = $this->getApplication()->find('demo:greet');
$arguments = [
'command' => 'demo:greet',
'name' => 'Fabien',
'yell' => true,
];
$returnCode = $command->run(new ArrayInput($arguments), $output);
use Symfony\Component\Form\Extension\Core\Type as Form;
public function newAction(Request $request)
{
$form = $this->createFormBuilder()
->add('name', Form\TextType::class)
->add('bio', Form\TextareaType::class)
->add('birthday', Form\DateType::class);
->getForm();
return $this->render('default/new.html.twig', [
'form' => $form->createView(),
]);
}
In order to display the Form, you need to pass a special view object to the
View layer. It's achieved through the createView()
method.
{# src/AppBundle/Resources/views/Default/new.html.twig #}
<form action="{{ path('acme_demo.default_new') }}" method="post">
{{ form_widget(form) }}
<input type="submit" />
</form>
When initially loading the page in a browser, the request method is GET and the form is simply created and rendered;
When the user submits the form (i.e. the method is POST) with invalid data, the form is bound and then rendered, this time displaying all validation errors;
When the user submits the form with valid data, the form is bound and you have the opportunity to perform some actions before redirecting the user to some other page (e.g. a "success" page).
Redirecting a user after a successful form submission prevents the user from being able to hit "refresh" and re-post the data.
use Symfony\Component\Form\Extension\Core\Type as Form;
public function newAction(Request $request)
{
$form = $this->createFormBuilder()
->add('name', Form\TextType::class)
->add('bio', Form\TextareaType::class)
->add('birthday', Form\DateType::class);
->getForm();
if ($form->handleRequest($request)->isValid()) {
$data = $form->getData();
// do something ...
return $this->redirectToRoute('success');
}
// ...
}
Everything is a Type!
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as Form;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', Form\TextType::class)
->add('bio', Form\TextareaType::class)
->add('birthday', Form\DateType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => My\Person::class,
]);
}
}
public function newAction(Request $request)
{
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
if ($form->handleRequest($request)->isValid()) {
$person->save(); // insert a new `person`
return $this->redirectToRoute('success');
}
// ...
}
Placing the form logic into its own class means that the form can be easily reused elsewhere in your project.
This is the best way to create forms, but the choice is up to you!
processForm()
Method (1/2)Saving or updating an object is pretty much the same thing. In order to avoid
code duplication, you can use a processForm()
method that can be used in both
the newAction()
and the updateAction()
:
/**
* Create a new Person
*/
public function newAction(Request $request)
{
return $this->processForm($request, new Person());
}
/**
* Update an existing Person
*/
public function updateAction(Request $request, $id)
{
$person = ...; // get a `Person` by its $id
return $this->processForm($request, $person);
}
processForm()
Method (2/2)/**
* @return Response
*/
private function processForm(Request $request, Person $person)
{
$form = $this->createForm(PersonType::class, $person);
if ($form->handleRequest($request)->isValid()) {
$person->save();
return $this->redirect($this->generateUrl('success'));
}
return $this->render('default/new.html.twig', [
'form' => $form->createView(),
]);
}
CSRF is a method by which a malicious user attempts to make your legitimate users unknowingly submit data that they don't intend to submit. Fortunately, CSRF attacks can be prevented by using a CSRF token inside your forms.
CSRF protection works by adding a hidden field to your form, called _token
by default that contains a value that only you and your user knows.
This ensures that the user is submitting the given data. Symfony automatically validates the presence and accuracy of this token.
The _token
field is a hidden field and will be automatically rendered if you
include the form_rest()
function in your template, which ensures that all
un-rendered fields are output.
<form action="" method="post" {{ form_enctype(form) }}>
{{ form_errors(form) }}
{{ form_row(form.name) }}
{{ form_row(form.bio) }}
{{ form_row(form.birthday) }}
{{ form_rest(form) }}
<input type="submit" />
</form>
Read more: https://symfony.com/doc/master/form/rendering.html.
form_enctype(form)
: if at least one field is a file upload field, this renders
the obligatory enctype="multipart/form-data"
;form_errors(form)
: renders any errors global to the whole form (field-specific
errors are displayed next to each field);form_row(form.name)
: renders the label, any errors, and the HTML form widget
for the given field inside, by default, a div element;form_rest(form)
: renders any fields that have not yet been rendered. It's
usually a good idea to place a call to this helper at the bottom of each form.
This helper is also useful for taking advantage of the automatic CSRF Protection.In the previous section, you learned how a form can be submitted with valid or invalid data. In Symfony, validation is applied to the underlying object.
In other words, the question isn't whether the "form" is valid, but whether the object is valid after the form has applied the submitted data to it.
Calling $form->isValid()
is a shortcut that asks the object whether it has
valid data using a Validation layer.
Validation is done by adding a set of rules (called constraints) to a class.
This component is based on the JSR303 Bean Validation specification.
Given the following class:
namespace AppBundle\Entity;
class Author
{
public $name;
}
You can configure a set of constraints on it:
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
properties:
name:
- NotBlank: ~
validator
Service$author = new Author();
// … do something to the $author object
$validator = $this->get('validator');
$errors = $validator->validate($author);
if (count($errors) > 0) {
// Ooops, errors!
} else {
// Everything is ok :-)
}
If the $name
property is empty, you will see the following error message:
AppBundle\Author.name:
This value should not be blank
Most of the time, you won't interact directly with the validator service or need to worry about printing out the errors. You will rather use validation indirectly when handling submitted form data.
http://symfony.com/doc/master/book/validation.html#constraints
Constraints can be applied to a class property or a public getter method
(e.g. getFullName()
). The first is the most common and easy to use, but the
second allows you to specify more complex validation rules.
Validating class properties is the most basic validation technique. Symfony allows you to validate private, protected or public properties.
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
properties:
firstName:
- NotBlank: ~
Some constraints apply to the entire class being validated. For example, the
Callback
constraint is a generic constraint that's applied to the class
itself.
Constraints can also be applied to the return value of a method. Symfony
allows you to add a constraint to any public method whose name starts with
get
or is
.
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
getters:
passwordLegal:
- 'IsTrue': { message: 'The password cannot match your first name' }
With the following code in the Author
class:
public function isPasswordLegal()
{
return $this->firstName !== $this->password;
}
In some cases, you will need to validate an object against only some of the constraints on that class.
You can organize each constraint into one or more validation groups, and then apply validation against just one group of constraints.
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\User:
properties:
email:
- Email: { groups: [ registration ] }
password:
- NotBlank: { groups: [ registration ] }
- Length: { groups: [ registration ], min: 7 }
city:
- Length:
min: 2
With the configuration seen before, there are two validation groups:
To tell the validator to use a specific group, pass one or more group names as
the second argument to the validate()
method:
$errors = $validator->validate($author, [ 'registration' ]);
If your object takes advantage of validation groups, you'll need to specify which validation group(s) your form should use:
$form = $this
->createFormBuilder($users, [
'validation_groups' => [ 'registration' ],
])
->add(...);
If you're creating form classes, then you'll need to add the following to
the configureOptions()
method:
use Symfony\Component\OptionsResolver\OptionsResolver;
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'validation_groups' => [ 'registration' ],
]);
}
use Symfony\Component\Validator\Constraints\Email;
$emailConstraint = new Email();
$emailConstraint->message = 'Invalid email address';
$errorList = $this->get('validator')->validateValue(
$email, $emailConstraint
);
if (0 !== count($errorList)) {
// this is *not* a valid email address
$errorMessage = $errorList[0]->getMessage();
}
By calling validateValue()
on the validator, you can pass in a raw value and
the constraint object that you want to validate that value against.
The validateValue()
method returns a ConstraintViolationList
object, which
acts just like an array of errors.
Each error in the collection is a ConstraintViolation
object, which holds the
error message on its getMessage()
method.
The term internationalization (often abbreviated i18n) refers to the process of abstracting strings and other locale-specific pieces out of your application and into a layer where they can be translated and converted based on the user's locale (i.e. language and country).
The act of creating translation files is an important part of localization (often abbreviated l10n). It is the process of adapting a product or service to a particular language, culture, and desired local look-and-feel.
translator
Service# messages.fr.yml
Symfony is great: J'aime Symfony
'Hello %name%': Bonjour %name%
When the following code is executed, Symfony will attempt to translate the
message Symfony is great
based on the locale of the user:
echo $this->get('translator')->trans('Symfony is great');
Now, if the language of the user's locale is French (e.g. fr_FR
or fr_BE
),
the message will be translated into J'aime Symfony
.
echo $this->get('translator')->trans('Hello %name%', [
'%name%' => 'Will'
]);
// French: Bonjour Will
// Default: Hello Will
To translate the message, Symfony uses a simple process:
The locale of the current user, which is stored on the request (or stored as
_locale
on the session), is determined;
A catalog of translated messages is loaded from translation resources defined
for the locale (e.g. fr_FR
). Messages from the fallback locale are also loaded
and added to the catalog if they don't already exist. The end result is a large
"dictionary" of translations;
If the message is located in the catalog, the translation is returned. If not, the translator returns the original message.
When using the trans()
method, Symfony looks for the exact string inside the
appropriate message catalog and returns it (if it exists).
Symfony looks for message files (i.e. translations) in the following locations:
<kernel root directory>/Resources/translations
directory;<kernel root directory>/Resources/<bundle name>/translations
directory;Resources/translations/
directory of the bundle.The filename of the translations is also important as Symfony uses a convention
to determine details about the translations. Each message file must be named
according to the following path: domain.locale.loader
:
domain
: an optional way to organize messages into groups;locale
: the locale that the translations are for (en_GB
, en
, etc);loader
: how Symfony should load and parse the file (xliff
, php
or yml
).When a translation has different forms due to pluralization, you can provide all
the forms as a string separated by a pipe (|
):
'There is one apple|There are %count% apples'
To translate pluralized messages, use the transChoice()
method:
$t = $this->get('translator')->transChoice(
'There is one apple|There are %count% apples',
10,
['%count%' => 10]
);
The second argument (10
in this example), is the number of objects being
described and is used to determine which translation to use and also to
populate the %count%
placeholder.
Sometimes, you'll need more control or want a different translation for specific cases (for 0, or when the count is negative, for example). For such cases, you can use explicit math intervals:
'{0} There are no apples|{1} There is one apple|]1,19] There are
%count% apples|[20,Inf[ There are many apples'
The intervals follow the ISO 31-11 notation.
An Interval can represent a finite set of numbers:
{1,2,3,4}
Or numbers between two other numbers:
[1, +Inf[
]-1,2[
# app/Resources/translations/Hello.fr.yml
ba:
bar: Bonjour.
place.holder: Bonjour %username%!
plural: Il y a %count% pomme|Il y a %count% pommes
<script src="{{ url('bazinga_jstranslation_js', { 'domain': 'Hello' }) }}">
</script>
A Translator
object is now available in your JavaScript:
Translator.trans('ba.bar', {}, 'Hello', 'fr');
// "Bonjour."
Translator.trans('place.holder', { "username" : "Will" }, 'Hello');
// "Bonjour Will!"
Translator.transChoice('plural', 1, { "count": 1 }, 'Hello');
// "Il y a 1 pomme"
Translator.transChoice('plural', 10, { "count": 10 }, 'Hello');
// "Il y a 10 pommes"
The nature of rich web applications means that they're dynamic. No matter how efficient your application, each request will always contain more overhead than serving a static file.
But as your site grows, that overhead can become a problem. The processing that's normally performed on every request should be done only once.
This is exactly what caching aims to accomplish!
A gateway cache, or reverse proxy, is an independent layer that sits in front of your application.
The reverse proxy caches responses as they are returned from your application and answers requests with cached responses before they hit your application.
Symfony provides its own reverse proxy, but any reverse proxy can be used.
HTTP cache headers are used to communicate with the gateway cache and any other caches between your application and the client.
Symfony provides sensible defaults and a powerful interface for interacting with the cache headers.
HTTP expiration and validation are the two models used for determining whether cached content is fresh (can be reused from the cache) or stale (should be regenerated by the application).
Edge Side Includes (ESI) allow HTTP cache to be used to cache page fragments (even nested fragments) independently. With ESI, you can even cache an entire page for 60 minutes, but an embedded sidebar for only 5 minutes.
When caching with HTTP, the cache is separated from your application entirely and sits between your application and the client making the request.
The job of the cache is to accept requests from the client and pass them back to your application.
The cache will also receive responses back from your application and forward them on to the client. The cache is the middle-man of the request-response communication between the client and your application.
Along the way, the cache will store each response that is deemed cacheable. If the same resource is requested again, the cache sends the cached response to the client, ignoring your application entirely.
This type of cache is known as a HTTP gateway cache and many exist such as Varnish, Squid in reverse proxy mode, and the Symfony reverse proxy.
The HTTP cache headers sent by your application are consumed and interpreted by up to three different types of caches:
HTTP specifies four response cache headers that are looked at here:
Cache-Control
Expires
ETag
Last-Modified
Both gateway and proxy caches are considered shared caches as the cached content is shared by more than one user.
If a user-specific response were ever mistakenly stored by a shared cache, it might be returned later to any number of different users. Imagine if your account information were cached and then returned to every subsequent user who asked for their account page!
To handle this situation, every response may be set to be public or private:
HTTP caching only works for safe HTTP methods (like GET
and HEAD
). Being
safe means that you never change the application's state on the server when
serving the request.
This has two very reasonable consequences:
GET
or HEAD
request. Even if you don't use a gateway cache, the presence
of proxy caches mean that any GET
or HEAD
request may or may not actually
hit your server;PUT
, POST
or DELETE
methods to cache. These methods are
meant to be used when mutating the state of your application. Caching them
would prevent certain requests from hitting and mutating your application.The expiration model is the more efficient and straightforward of the two caching models and should be used whenever possible.
When a response is cached with an expiration, the cache will store the response and return it directly without hitting the application until it expires.
The expiration model can be accomplished using one of two HTTP headers:
Cache-Control
Expires
The Cache-Control
header is unique in that it contains not one, but
various pieces of information about the cacheability of a response.
Each piece of information is separated by a comma:
Cache-Control: private, max-age=0, must-revalidate
Cache-Control: max-age=3600, must-revalidate
Symfony provides an abstraction around the Cache-Control
header:
use Symfony\Component\HttpFoundation\Response;
$response = new Response();
// mark the response as either public or private
$response->setPublic();
$response->setPrivate();
// set the private or shared max age
$response->setMaxAge(600);
$response->setSharedMaxAge(600);
// set a custom Cache-Control directive
$response->headers->addCacheControlDirective('must-revalidate', true);
The Expires
header can be set with the setExpires()
Response method. It takes
a DateTime
instance as an argument:
$date = new DateTime();
$date->modify('+600 seconds');
$response->setExpires($date);
The resulting HTTP header will look like this:
Expires: Thu, 01 Mar 2013 10:00:00 GMT
The setExpires()
method automatically converts the date to the GMT timezone as
required by the specification.
With the expiration model, the application won't be asked to return the updated response until the cache finally becomes stale. It is not good!
The validation model addresses this issue.
Under this model, the cache continues to store responses. The difference is that, for each request, the cache asks the application whether or not the cached response is still valid.
If the cache is still valid, your application should return a 304
status code
and no content. This tells the cache that it's ok to return the cached response.
The ETag
header is a string header called entity-tag that uniquely
identifies one representation of the target resource. It's entirely
generated and set by your application.
ETag
s are similar to fingerprints and they can be quickly compared to
determine if two versions of a resource are the same or not.
public function indexAction()
{
$response = $this->render('main/index.html.twig');
$response->setETag(md5($response->getContent()));
$response->setPublic(); // make sure the response is public/cacheable
$response->isNotModified($this->getRequest());
return $response;
}
The isNotModified()
method compares the ETag
sent with the Request
with
the one set on the Response
. If the two match, the method automatically sets
the Response
status code to 304 Not Modified
.
According to the HTTP specification, the Last-Modified header field indicates the date and time at which the origin server believes the representation was last modified.
In other words, the application decides whether or not the cached content has been updated based on whether or not it's been updated since the response was cached.
public function showAction($articleSlug)
{
// ...
$articleDate = new \DateTime($article->getUpdatedAt());
$authorDate = new \DateTime($author->getUpdatedAt());
$date = $authorDate > $articleDate ? $authorDate : $articleDate;
$response->setLastModified($date);
// Set response as public. Otherwise it will be private by default
$response->setPublic();
if ($response->isNotModified($this->getRequest())) {
return $response;
}
// ... do more work to populate the response
// with the full content
return $response;
}
Edge Side Includes or ESI is a small markup language for dynamic web content assembly at the reverse proxy level. The reverse proxy analyses the HTML code, parses ESI specific markup and assembles the final result before flushing it to the client.
<esi:include src="user.php" />
Table of contents | t |
---|---|
Exposé | ESC |
Autoscale | e |
Full screen slides | f |
Presenter view | p |
Edit the current slide | s |
Slide numbers | n |
Blank screen | b |
Notes | 2 |
Help | h |