00:00:00

PHP Extended

Notes

Let's Do Professional Development Now!

Notes

Who Is Speaking?

Notes

Kévin Gomez

(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:

twitter.com/KPhoen  |  github.com/K-Phoen  |  kevingomez.fr

Notes

Slides originally written by William Durand

Notes

Agenda

  • Security basics
  • Symfony
    • Controllers
    • Templating
    • Dependency Injection
    • Command Line
    • Forms
    • Validation
    • Translation
    • HTTP Cache

Notes

Security basics

Notes

OWASP – Open Web Application Security Project

  • Not-for-profit organization focused on improving the security of software;
  • Documentation and tools to help learn about security, and protect your applications.

https://www.owasp.org

Notes

OWASP Top 10 – 2017

  1. Injection
  2. Broken Authentication and Session Management
  3. Cross-Site Scripting (XSS)
  4. Broken Access Control
  5. Security Misconfiguration
  6. Sensitive Data Exposure ← won't be covered here
  7. Insufficient Attack Protection ← won't be covered here
  8. Cross-Site Request Forgery (CSRF)
  9. Using Components with Known Vulnerabilities
  10. Underprotected APIs ← won't be covered here

https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project

Notes

1. Injection

Notes

OWASP definition

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.

https://www.owasp.org/index.php/Category:Injection

Notes

In other words

  • When an application sends untrusted data to an interpreter;
  • Attacker sends simple text-based attacks that exploit the syntax of the targeted interpreter;
  • Almost any source of data can be an attack vector:
    • GET/POST parameters
    • PATH_INFO
    • HTTP headers
    • uploaded files
  • Can result in data loss or corruption or denial of access.

Notes

SQL injection

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 --

Notes

Directory traversal attack

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

Remote Code Execution

Unsafe input is dynamically executed:

exec('rm -rf web/upload/' . $_GET['file']);
// example.org?file=*; rm -rf /;

Notes

How To Prevent Injections?

Escape, escape, escape!

  • SQL: Prepared Statements (PDO);
  • Input/Output: basename(), realpath();
  • HTML: htmlspecialchars() (or better: use a template engine that escapes everything by default);
  • System calls: shell_escape_args();
  • Cookies: make sure they contain what you expect;
  • Validate all user input;
  • Positive or white-list input validation;
  • Avoid eval() or exec() functions.

Notes

2. Broken Authentication and Session Management

Notes

In A Nutshell

  • Attackers use leaks or flaws in the authentication or session management functions to impersonate users;
  • May allows some or even all accounts to be attacked. Once successful, the attacker can do anything the victim could do.
  • Privileged accounts are frequently targeted!

Notes

How To Prevent This?

  • Hash and salt passwords properly (bcrypt/Blowfish please);
  • Never ever store passwords in clear text;
  • Don't put session IDs in URLs;
  • Regenerate IDs when authentication changes;
  • Allow session IDs to timeout/expire;
  • Use HTTPS. ALWAYS.

Notes

PHP Session Configuration

; 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

Notes

3. Cross-Site Scripting (XSS)

Notes

In A Nutshell

  • The most prevalent web application security flaw!
  • When an application includes user supplied data in a page sent to the browser without properly validating or escaping that content;
  • Attacker sends text-based attack scripts that exploit the interpreter in the browser;
  • Almost any source of data can be an attack vector, including internal sources such as data from the database, but also URLs, form fields;
  • Can result in session hijacking, malicious scripts execution, website defacement.

Notes

How To Prevent XSS?

Do not trust anyone!

  • Escape all untrusted data based on the HTML context will go through htmlspecialchars();
  • Whitelist input validation;
  • Consider auto-sanitization libraries (HTML Purifier);
  • Don't even trust admins;
  • Secure your cookies (encrypt or sign them);
  • Use template engines that escape everything by default (like Twig).

Notes

Content Security Policy (CSP)

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

An Introduction to Content Security Policy

Using Content-Security-Policy for Evil

Notes

4. Broken Access Control

Notes

OWASP definition

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.

Notes

How To Prevent This?

  • Always check the user credentials before allowing
    access
    to restricted content.

Notes

5. Security Misconfiguration

Notes

In A Nutshell

  • Attacker accesses default accounts, unused pages, unpatched flaws, unprotected files and directories, etc. to gain unauthorized access to or knowledge of the system;
  • Can completely compromise the system without you knowing it. All of your data can be stolen or modified slowly over time;
  • Recovery costs can be expensive!

Notes

How To Prevent This?

  • Have a process for keeping on top of updates and patches;
  • Disable risky PHP native functions (shell_exec, …)
  • Build a strong application architecture that provides effective and secure separation between components;
  • Consider running scans and doing audits periodically;
  • Let your webserver process run by a user with restricted permission. Never as root!

Notes

8. Cross-Site Request Forgery (CSRF)

Notes

In A Nutshell

  • Attacker creates forged HTTP requests and tricks a victim into submitting them via image tags, XSS, Form POSTs, or numerous other techniques. If the user is authenticated, the attack succeeds;
  • Attackers can create malicious web pages which generate forged requests that are indistinguishable from legitimate ones;
  • Causes victim to change any data the victim is allowed to change, but also to perform any function the victim is authorized to use.

Facebook CSRF worth USD 5000
Does Google Understand CSRF?
A few CSRF-like vulnerable examples.

Notes

CSRF With GET

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>

Notes

CSRF With POST

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();
});

Notes

How To Prevent This?

  • Don't do "things" using the GET method;
  • Create one unique token per user (at least once per session);
    • Include/verify it in every sensitive form;
    • Avoid putting this token in the query string.

Notes

Same-Origin Policy

The same-origin policy restricts how a document or script loaded from one origin can interact with a resource from another origin.


Notes

9. Using Components with Known Vulnerabilities

Notes

In A Nutshell

  • Attacker identifies a weak component through scanning or manual analysis. He customizes the exploit as needed and executes the attack;
  • Virtually every application has these issues because most development teams don't focus on ensuring their libraries
    are up to date;
  • In many cases, the developers don't even know all the components they are using, never mind their versions. Component dependencies make things even worse;
  • The impact could range from minimal to complete host takeover and data compromise.

Notes

How To Prevent This?

Notes

Notes

A Framework To Simplify Developments

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.

Notes

What Is Symfony?

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.

Notes

Is Symfony A MVC Framework?

Notes

NO!

Notes

Why You Should Use Symfony

Symfony is built on powerful concepts:

  • Separation of Concerns;
  • Pragmatism;
  • Best Practices.

It has been written by ~1679 developers.

Open Source, MIT licensed.

Notes

The Symfony Components

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

https://symfony.com/components

Notes

Getting Ready With Components

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

Notes

Full-Stack Framework

The Symfony Framework accomplishes two distinct tasks:

  • Provides a selection of components;
  • Provides sensible configuration and a "glue" library that ties all of these pieces together.

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.

http://symfony.com/doc/current/book/index.html

Notes

Overall Architecture

Notes

The Symfony Request

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');

Notes

The Symfony Response

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();

Notes

The Simplest Front Controller Ever

// 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();

Notes

The Symfony Application Flow

It's all about transforming a Request into a Response:

Notes

Routing Definition

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.

Notes

Your First Controller

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>');
    }
}

Notes

A Symfony Project (1/2)

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
        ...

Notes

A Symfony Project (2/2)

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.

Notes

Application Kernel

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;
    }

    // ...
}

Notes

Application Configuration

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.

Notes

YAML Configuration

Example:

# 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%'

# ...

Notes

XML Configuration

Example:

<!-- 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%"/>

<!-- ... -->

Notes

PHP Configuration

Example:

$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%',
]);

// ...

Notes

The Rules (Well... My Rules)

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

Notes

Environments

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'
    );
}

Notes

What Is A Bundle?

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(),
    ];

    // ...
}

Notes

Bundle: Directory Structure

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.

Notes

Bundle: Where To Put Your Classes?

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/

Notes

Creating a Bundle

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;
}

Notes

The Web Directory

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.

Notes

Summary

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.

Notes

Read The Best Practices!

Notes

Controllers

Notes

Request, Controller, Response

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:

  1. Each request is handled by a single front controller file (e.g. app.php or app_dev.php) that bootstraps the application;
  2. The Router reads information from the request (e.g. the URI), finds a route that matches that information, and reads the _controller parameter from the route;
  3. The controller from the matched route is executed and the code inside the controller creates and returns a Response object;
  4. The HTTP headers and content of the Response object are sent back to the client.

Notes

The Simplest Page Ever

Routing Definition

# app/config/routing.yml
homepage:
    path:  /
    defaults: { _controller: AppBundle:Hello:index }

Controller Implementation

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function indexAction()
    {
        return new Response('Home, Sweet Home!');
    }
}

Notes

Controller Naming Pattern

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:

  • Bundle: AcmeBlogBundle;
  • Controller Class: BlogController;
  • Method Name: showAction.

Notice that Symfony adds the string Controller to the class name (Blog => BlogController) and Action to the method name (show => showAction).

Notes

Route Params as Controller Args

Routing Definition

# src/AppBundle/Resources/config/routing.yml
app.hello_hello:
    path:  /hello/{name}
    defaults: { _controller: AppBundle:Hello:hello }
    requirements:
        _method: GET

Controller Implementation

// src/AppBundle/Controller/HelloController.php

class HelloController
{
    // ...

    public function helloAction($name)
    {
        return new Response(sprintf('Home, Sweet %s!', $name));
    }
}

Notes

The Request as a Controller Argument

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.

Notes

The Base Controller Class

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
{
    // ...
}

Redirecting

$this->redirect($this->generateUrl('homepage')); // see also: redirectToRoute

Rendering Templates

return $this->render('hello/hello.html.twig', [
    'name' => $name
]);

Notes

The Response

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]);

Notes

Routing

Notes

Basic Route Configuration

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.

http://symfony.com/doc/master/book/routing.html

Notes

Routing with Placeholders (1/2)

Required Placeholders

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.

Notes

Routing with Placeholders (2/2)

Optional Placeholders

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.

Notes

Requirements

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).

HTTP Method Requirements

# src/AppBundle/Resources/config/routing.yml
app.hello_hello:
    path:  /hello/{name}
    defaults: { _controller: 'AppBundle:Hello:hello' }
    methods:  [ GET ]
    # methods:  [ GET, POST ]

Notes

Including External Routing Resources

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'

Prefixing Imported Routes

# 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.

Notes

Generating URLs

The Router is able to generate both relative and absolute URLs.

$router = $this->get('router');

Relative URLs

$router->generate('app.hello_hello', [ 'name' => 'will' ]);
// /hello/will

Absolute URLs

$router->generate('app.hello_hello', [ 'name' => 'will' ], true);
// http://example.com/hello/will

Query String

$router->generate('app.hello_hello', [
    'name' => 'will', 'some' => 'thing'
]);
// /hello/will?some=thing

Notes

Templating

Notes

Notes

Why Twig?

Fast, Secure, Flexible.

Before

<ul id="navigation">
    <?php foreach ($navigation as $item): ?>
        <li>
            <a href="<?php echo $item->getHref() ?>">
                <?php echo $item->getCaption() ?>
            </a>
        </li>
    <?php endforeach; ?>
</ul>

After

<ul id="navigation">
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
    {% endfor %}
</ul>

Notes

Getting Familiar With Twig

Delimiters

  • {{ ... }}: 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.

http://twig.sensiolabs.org/

Notes

Accessing Variables

{# 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') }}

Notes

Control Structure

Conditions

{% if user.isSuperAdmin() %}
    ...
{% elseif user.isMember() %}
    ...
{% else %}
    ...
{% endif %}

Loops

<ul>
    {% for user in users if user.active %}
        <li>{{ user.username }}</li>
    {% else %}
        <li>No users found</li>
    {% endfor %}
</ul>

Notes

Filters

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') }}

http://twig.sensiolabs.org/doc/filters/index.html

Notes

Including Other Templates

The include tag is useful to include a template and return the rendered content of that template into the current one:

{% include 'sidebar.html' %}

Example

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>

Notes

Template Inheritance (1/2)

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>

Notes

Template Inheritance (2/2)

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.

Notes

Template Naming and Locations (1/2)

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.

Notes

Template Naming and Locations (2/2)

Example

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

Notes

Overriding Bundle Templates

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.

Notes

Overriding Core Templates

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.

Notes

Twig Into Symfony

Notes

Rendering A Template

Using The Base Controller

public function listAction()
{
    // ...

    return $this->render('blog/index.html.twig', [
        'posts' => $posts,
    ]);
}

Using the Templating Service

$engine  = $this->container->get('templating');
$content = $engine->render('blog/index.html.twig', [
    'posts' => $posts,
]);

return new Response($content);

Notes

Linking to Pages

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'}) }}">

Notes

Linking to Assets

<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

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

Notes

Linking To Pages In JavaScript

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

Notes

Global Template Variables

  • 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.

Notes

Service Container

Notes

What Is A Service?

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).

Notes

What Is A Service Container?

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.

Notes

Creating A Service

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');

Notes

Service Parameters

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.

Notes

Injecting Services

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%'

Optional Dependencies: Setter Injection

calls:
    - [setBar, ['@bar']]

Notes

Importing Configuration Resources

The imports Way

# app/config/config.yml
imports:
    - { resource: "@AcmeDemoBundle/Resources/config/services.yml" }

Container Extensions

A service container extension is a PHP class to accomplish two things:

  • import all service container resources needed to configure the services for the bundle;
  • provide semantic, straightforward configuration so that the bundle can be configured without interacting with the flat parameters of the bundle's service container configuration.

Notes

Creating an Extension Class

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');
    }
}

Notes

Dealing With Configuration (1/2)

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',
    )
)

Notes

Dealing With Configuration (2/2)

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
}

Notes

The Configuration Class (1/2)

Definition

// 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;
    }
}

Notes

The Configuration Class (2/2)

Usage

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.

Notes

More On The Service Container

Tags

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.

Debugging Services

$ php bin/console debug:container
$ php bin/console debug:container foo

http://symfony.com/doc/master/book/service_container.html

Notes

Symfony Commands

Notes

Built-in Commands (1/2)

$ php bin/console

Global Options

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

Notes

Built-in Commands (2/2)

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

Notes

Creating Commands

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 ...
    }
}

Notes

Command Arguments

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?'
        );
}

Usage

// php bin/console demo:greet Kévin Gomez
$input->getArgument('last_name');

Notes

Command Options (1/2)

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'
        );
}

Usage

// php bin/console demo:greet --yell

if ($input->getOption('yell')) {
    // ...
}

Notes

Command Options (2/2)

protected function configure()
{
    $this
        // ...
        ->addOption(
            'iterations', null, InputOption::VALUE_REQUIRED,
            'How many times should the message be printed?',
            1
        );
}

Usage

// php bin/console demo:greet --iterations=10

for ($i = 0; $i < $input->getOption('iterations'); $i++) {
}

Notes

More On Commands

Getting Services from the Service Container

protected function execute(InputInterface $input, OutputInterface $output)
{
    $translator = $this->getContainer()->get('translator');
    // ...
}

Calling an existing Command

$command = $this->getApplication()->find('demo:greet');
$arguments = [
    'command' => 'demo:greet',
    'name'    => 'Fabien',
    'yell'    => true,
];

$returnCode = $command->run(new ArrayInput($arguments), $output);

http://symfony.com/doc/master/cookbook/console/index.html

Notes

Forms

Notes

Building Your First Form

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.

Notes

Rendering The Form


{# src/AppBundle/Resources/views/Default/new.html.twig #}
<form action="{{ path('acme_demo.default_new') }}" method="post">
    {{ form_widget(form) }}

    <input type="submit" />
</form>

Notes

Handling Forms: The Right Way

  1. When initially loading the page in a browser, the request method is GET and the form is simply created and rendered;

  2. 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;

  3. 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.

Notes

Handling Form Submissions

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');
    }

    // ...
}

Notes

Built-in Form Types

Everything is a Type!

Notes

Creating A Custom Type (Form Class)

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,
        ]);
    }
}

Notes

Dealing With Objects

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!

Notes

The 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);
}

Notes

The 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(),
    ]);
}

Notes

Cross-Site Request Forgery Protection

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.

Notes

Rendering a Form in a Template (1/2)

<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.

Notes

Rendering a Form in a Template (2/2)

  • 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.

Notes

Validation

Notes

About Form Validation

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.

Notes

The Validator Component

This component is based on the JSR303 Bean Validation specification.

Example

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: ~

Notes

Using the 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.

Notes

Notes

Constraint Targets (1/2)

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.

Properties

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: ~

Classes

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.

Notes

Constraint Targets (2/2)

Getters

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;
}

Notes

Validation Groups (1/2)

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.

Example

# 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

Notes

Validation Groups (2/2)

With the configuration seen before, there are two validation groups:

  • Default: contains the constraints not assigned to any other group;
  • registration: contains the constraints on the email and password fields only.

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' ]);

Notes

Using Validation Groups In Forms

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' ],
    ]);
}

Notes

Validating Values and Arrays

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.

Notes

Translations

Notes

Definitions

Internationalization

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).

Localization

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.

Notes

Using the 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.

Message Placeholders

echo $this->get('translator')->trans('Hello %name%', [
    '%name%' => 'Will'
]);

// French:  Bonjour Will
// Default: Hello Will

Notes

The Translation Process

To translate the message, Symfony uses a simple process:

  1. The locale of the current user, which is stored on the request (or stored as _locale on the session), is determined;

  2. 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;

  3. 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).

Notes

Locations and Naming Conventions

Symfony looks for message files (i.e. translations) in the following locations:

  • the <kernel root directory>/Resources/translations directory;
  • the <kernel root directory>/Resources/<bundle name>/translations directory;
  • the 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).

Notes

Pluralization

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.

Notes

Explicit Interval Pluralization

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[

Notes

BazingaJsTranslationBundle

# 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"

Notes

HTTP Cache

Notes

HTTP Cache

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!

Notes

Terminology (1/2)

Gateway Cache

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

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.

Notes

Terminology (2/2)

HTTP Expiration

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

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.

Notes

Caching with a Gateway Cache

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.

Notes

Types of Caches

The HTTP cache headers sent by your application are consumed and interpreted by up to three different types of caches:

  • Browser Caches: every browser comes with its own local cache that is mainly useful for when you hit "back" or for images and other assets. The browser cache is a private cache as cached resources aren't shared with anyone else;
  • Proxy Caches: a proxy is a shared cache as many people can be behind a single one. It's usually installed by large corporations and ISPs to reduce latency and network traffic;
  • Gateway Caches: like a proxy, it's also a shared cache but on the server side. Installed by network administrators, it makes websites more scalable, reliable and performant.

Notes

HTTP Caching

HTTP specifies four response cache headers that are looked at here:

  • Cache-Control
  • Expires
  • ETag
  • Last-Modified

Notes

Public vs Private Responses

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:

  • public: indicates that the response may be cached by both private and shared caches;
  • private: indicates that all or part of the response message is intended for a single user and must not be cached by a shared cache.

Notes

Safe Methods

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:

  • You should never change the state of your application when responding to a 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;
  • Don't expect 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.

Notes

Expiration

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

Notes

The Cache-Control Header (1/2)

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

Notes

The Cache-Control Header (2/2)

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);

Notes

The Expires Header

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.

Notes

Validation

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.

Notes

The ETag Header

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.

ETags 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.

Notes

The Last-Modified Header (1/2)

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.

Notes

The Last-Modified Header (2/2)

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;
}

Notes

Edge Side Includes (ESI)

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" />

Notes

The End.

Notes

Fork me on GitHub