Symfony 3

HTTP Fundamentals

HTTP 1.1

Request / Response

Request


GET / HTTP/1.1
Host: xkcd.com
Accept: text/html
User-Agent: Mozilla/5.0 (Macintosh)

Request

GET ​Retrieve the resource from the server
POST Create a resource on the server
PUT Update the resource on the server
DELETE Delete the resource from the server
GET /blog/15 HTTP/1.1
DELETE /blog/15 HTTP/1.1
POST /blog HTTP/1.1

Response

HTTP/1.1 200 OK
Date: Sat, 02 Apr 2011 21:05:05 GMT
Server: lighttpd/1.4.19
Content-Type: text/html

<html>
  <!-- ... HTML for the xkcd comic -->
</html>

Req/Resp in PHP



$uri = $_SERVER['REQUEST_URI'];
$foo = $_GET['foo'];

header('Content-Type: text/html');

echo 'The URI requested is: '.$uri;
echo 'The value of the "foo" parameter is: '.$foo;

Req/Resp in PHP


HTTP/1.1 200 OK
Date: Sat, 03 Apr 2011 02:14:33 GMT
Server: Apache/2.2.17 (Unix)
Content-Type: text/html

The URI requested is: /testing?foo=symfony
The value of the "foo" parameter is: symfony

Req/Resp in Symfony

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$uri = $request->getPathInfo();
$foo = $request->get('foo');

$response = new Response();

$body = 'The URI requested is: '.$uri;
$body .= 'The value of the "foo" parameter is: '.$foo;

$response->setContent($body);
$response->setStatusCode(Response::HTTP_OK);

$response->headers->set('Content-Type', 'text/html');
$response->send();

Application Flow

Sample Controller

namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class SampleController
{
    /**
     * @Route("/sample")
     */
    public function indexAction()
    {
        return new Response('<h1>Some beautiful message!</h1>');
    }
}

Creating the Symfony Application

Creating App

$ symfony new my_project_name

Symfony Installer

$ composer create-project symfony/framework-standard-edition my_project_name

Composer

Running


$ cd my_project_name/
$ php bin/console server:run

localhost:8000

Controller

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

class LuckyController
{
    /**
     * @Route("/lucky/number")
     */
    public function numberAction()
    {
        $number = rand(0, 100);

        return new Response('Lucky number: '.$number);
    }
}

localhost:8000/lucky/number

Playground

Twig

Template Engine

Syntax

Prints a variable or the result of an expression to the template.


{{ ... }}

A tag that controls the logic of the template; it is used to execute statements such as for-loops for example.


{% ... %}

{# ... #}

It's the equivalent of the PHP /* comment */ syntax.

Loop

<ul>
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
    {% endfor %}
</ul>
<ul>
    {% for i in 0..10 %}
        <li>Number {{ i }}</li>
    {% endfor %}
</ul>
<ul>
    {% for user in users if user.active %}
        <li>{{ user.username }}</li>
    {% else %}
        <li>No users found</li>
    {% endfor %}
</ul>

Conditional



{% if app.user %}
    <div>
        Hello {{ app.user }}
    </div>
{% else %}
    <div>
        Please sign in
    </div>
{% endif %}

Inheritance

{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Template inheritance</title>
    </head>
    <body>

        <div id="content">
            {% block body %}
                No body defined!!!
            {% endblock %}
        </div>
    </body>
</html>

Inheritance

{# app/Resources/views/blog/index.html.twig #}

{% extends 'base.html.twig' %}

{% block body %}

    {% for entry in blog_entries %}

        <h2>{{ entry.title }}</h2>
        <p>{{ entry.body }}</p>

    {% endfor %}

{% endblock %}

Filters

{{ myVar | upper }}


{{ myDate | date("m/d/Y") }}


{{ myObj | json_encode }}


{{ myArray | length }}


{{ myVar | raw }}


{{ numbers | reverse | join(',') }}

Controller template


/**
 * @Route("/lucky/number/{count}")
 */
public function numberAction($count)
{
    $number = rand(0, 100);

    return $this->render('lucky/number.html.twig', [
        'luckyNumber' => $number
    ]);
}

Controller template

{# app/Resources/views/lucky/number.html.twig #}

{% extends 'base.html.twig' %}

{% block body %}
    <h1>Lucky Number: {{ luckyNumber }}</h1>
{% endblock %}

Playground

Routing

Name

/**
 * @Route("/", name="blog_homepage")
 */
public function indexAction()
{
    // ...
}

Placeholder

/**
 * @Route("/blog/{slug}")
 */
public function showAction($slug)
{
    // ...
}

Defaults

/**
 * @Route("/blog/{page}", defaults={"page": 1})
 */
public function indexAction($page)
{
    // ...
}
/**
 * @Route("/blog/{page}")
 */
public function indexAction($page = 1)
{
    // ...
}

Requirements

/**
 * @Route("/blog/{page}", defaults={"page": 1}, requirements={
 *     "page": "\d+"
 * })
 */
public function indexAction($page)
{
    // ...
}
/**
 * @Route("/{_locale}", defaults={"_locale": "en"}, requirements={
 *     "_locale": "en|fr"
 * })
 */
public function homepageAction($_locale)
{
    // ...
}

HTTP Method

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

/**
 * @Route("/api/posts/{id}")
 * @Method({"GET","HEAD"})
 */
public function showAction($id)
{
    // ...
}
/**
 * @Route("/api/posts/{id}")
 * @Method("PUT")
 */
public function editAction($id)
{
    // ... 
}

Advanced

/**
 * @Route(
 *     "/articles/{_locale}/{year}/{title}.{_format}",
 *     defaults={"_format": "html"},
 *     requirements={
 *         "_locale": "en|fr",
 *         "_format": "html|rss",
 *         "year": "\d+"
 *     }
 * )
 * @Method("GET")
 */
public function showAction($_locale, $year, $title)
{
    // ...
}

Special Parameters

As you've seen, this parameter is used to determine which controller is executed when the route is matched.

_format

_locale

_controller

Used to set the request format.

Used to set the locale on the request.

Debugging


$ bin/console debug:router

homepage              ANY       /
contact               GET       /contact
contact_process       POST      /contact
article_show          ANY       /articles/{_locale}/{year}/{title}.{_format}
blog                  ANY       /blog/{page}
blog_show             ANY       /blog/{slug}

Playground

Controller

Controller

The goal of a controller is always the same: create and return a Response object

use Symfony\Component\HttpFoundation\Response;

public function helloAction()
{
    return new Response('Hello world!');
}

Responses

public function helloAction()
{
    return $this->render('hello.html.twig');
}
use Symfony/Component/HttpFoundation/JsonResponse;

public function helloAction()
{
    return new JsonResponse([
        'hello' => 'Hello human!!!'
    ]);
}
public function helloAction()
{
    return new Response('<h1>Response content</h1>');
}

Responses

public function helloAction()
{
    return $this->redirectToRoute('another_route_name');
}
public function helloAction()
{
    return $this->forward('another_route_name');
}
public function helloAction()
{
    return $this->redirect('http://symfony.com/doc');
}

Request Parameter

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $request->isXmlHttpRequest(); // is it an Ajax request?

    // request parameter
    $request->get('page');

    // retrieves an instance of UploadedFile identified by foo
    $request->files->get('foo');

    // retrieve an HTTP request header, with normalized, lowercase keys
    $request->headers->get('host');
    $request->headers->get('content_type');
}

Request Parameter

use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route('/{name}')
 *
 */
public function indexAction(Request $request, $name)
{
    return $this->render('index.html.twig', [ 'name' => $name ]);
}

Session

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $session = $request->getSession();

    // store an attribute for reuse during a later user request
    $session->set('foo', 'bar');

    // get the attribute set by another controller in another request
    $foobar = $session->get('foobar');

    // use a default value if the attribute doesn't exist
    $filters = $session->get('filters', array());
}

Flash Messages

$this->addFlash(
    'notice',
    'Your changes were saved!'
);
{% for message in app.session.flashBag.get('notice') %}
    <div class="alert alert-info">
        {{ message }}
    </div>
{% endfor %}

Playground

Database

& Doctrine

Connection info

# app/config/parameters.yml
parameters:
    database_host:      localhost
    database_name:      test_project
    database_user:      root
    database_password:  password

Generate Entity


$ bin/console doctrine:generate:entity

Entity Mapping

// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="product")
 */
class Product
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private $name;
}

Creating Tables


$ bin/console doctrine:schema:update --dump-sql

$ bin/console doctrine:schema:update --force

Fetching

// src/AppBundle/Controller/DefaultController.php

use AppBundle\Entity\Product;

public function showAction($productId)
{
    $product = $this->getDoctrine()
        ->getRepository(Product::class)
        ->find($productId);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$productId
        );
    }

    // ... do something, like pass the $product object into a template
}

Persisting

// src/AppBundle/Controller/DefaultController.php

use AppBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

public function createAction()
{
    $product = new Product();
    $product->setName('Keyboard');
    $product->setPrice(19.99);
    $product->setDescription('Ergonomic and stylish!');

    $em = $this->getDoctrine()->getManager();

    // tells Doctrine you want to (eventually) save the Product (no queries yet)
    $em->persist($product);

    // actually executes the queries (i.e. the INSERT query)
    $em->flush();

    return new Response('Saved new product with id '.$product->getId());
}

Updating

// src/AppBundle/Controller/DefaultController.php

use AppBundle\Entity\Product;

public function updateAction($productId)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository(Product::class)->find($productId);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$productId
        );
    }

    $product->setName('New product name!');

    $em->merge($product);
    $em->flush();

    return $this->redirectToRoute('homepage');
}

Deleting

// src/AppBundle/Controller/DefaultController.php

use AppBundle\Entity\Product;

public function deleteAction($productId)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository(Product::class)->find($productId);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$productId
        );
    }

    $em->remove($product);
    $em->flush();

    return $this->redirectToRoute('homepage');
}

Querying

// src/AppBundle/Controller/DefaultController.php

use AppBundle\Entity\Product;

public function searchAction()
{
    $em = $this->getDoctrine()->getManager();

    $query = $em->createQuery(
         'SELECT p
          FROM AppBundle:Product p
          WHERE p.price > :price
          ORDER BY p.price ASC'
    )->setParameter('price', 19.99);

    $products = $query->getResult();

    // ...
}

Querying

// src/AppBundle/Controller/DefaultController.php

use AppBundle\Entity\Product;

public function searchAction()
{
    $em = $this->getDoctrine()->getManager();

    $query = $em->createQueryBuilder()
                ->select('p')
                ->from(Product::class, 'p')
                ->where('p.price > :price')
                ->orderBy('p.price', 'ASC')
                ->setParameter('price', 19.99)
                ->getQuery();

    $products = $query->getResult();

    // ...
}

Relationship

// src/AppBundle/Entity/Product.php

// ...
class Product
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;
}

Relationship

// src/AppBundle/Entity/Category.php

// ...
use Doctrine\Common\Collections\ArrayCollection;

class Category
{
    // ...

    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
     */
    private $products;

    public function __construct()
    {
        $this->products = new ArrayCollection();
    }
}

Playground

Forms

Form Builder

// controller class

$task = new Task();

$form = $this->createFormBuilder($task)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, ['label' => 'Create Task'])
        ->getForm();

Rendering

{# form page #}

{{ form_start(form) }}

{{ form_widget(form) }}

{{ form_end(form) }}

Handling Form

// controller class

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    // ... perform some action, such as saving the task to the database

    return $this->redirectToRoute('task_success');
}

Field Types

  • TextType
  • TextareaType
  • EmailType
  • IntegerType
  • MoneyType
  • NumberType
  • PasswordType
  • PercentType
  • SearchType
  • UrlType
  • RangeType

Text fields

Field Types

  • ChoiceType
  • EntityType
  • CountryType
  • LanguageType
  • LocaleType
  • TimezoneType
  • CurrencyType

Choice fields

Field Types

  • DateType
  • DateTimeType
  • TimeType
  • BirthdayType

Date and time fields

Custom render

{{ form_start(form) }}

    {{ form_errors(form) }}

    {{ form_row(form.task) }}

    {{ form_row(form.dueDate) }}

{{ form_end(form) }}

Custom render

{{ form_start(form) }}
    {{ form_errors(form) }}

    <div>
        {{ form_label(form.task) }}
        {{ form_errors(form.task) }}
        {{ form_widget(form.task) }}
    </div>

    <div>
        {{ form_label(form.dueDate) }}
        {{ form_errors(form.dueDate) }}
        {{ form_widget(form.dueDate) }}
    </div>

    <div>
        {{ form_widget(form.save) }}
    </div>

{{ form_end(form) }}

Form class

// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', SubmitType::class)
        ;
    }
}

Form class

// controller

use AppBundle\Form\TaskType;

public function newAction()
{
    $task = ...;
    $form = $this->createForm(TaskType::class, $task);

    // ...
}

Form generator


$ bin/console doctrine:generate:form AppBundle:Product

Bootstrap theme

# app/config/config.yml

twig:
    form_themes: ['bootstrap_3_layout.html.twig']

Validation

// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Task
{
    /**
     * @Assert\NotBlank()
     */
    public $task;

    /**
     * @Assert\NotBlank()
     * @Assert\Type("\DateTime")
     */
    protected $dueDate;
}

Validation

// src/AppBundle/Form/TaskForm.php

use Symfony\Component\Validator\Constraints as Assert;

$builder
   ->add('task', null, [
       'constraints' => [
           new Assert\NotBlank(),
           new Assert\Length([ 'min' => 3 ]),
       ],
   ])
   ->add('lastName', null, [
       'constraints' => [
           new Assert\NotBlank(),
           new Assert\Type("\DateTime")
       ),
   ])
;

File upload

// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

class Product
{
    // ...

    /**
     * @ORM\Column(type="string")
     *
     * @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.")
     * @Assert\File(mimeTypes={ "application/pdf" })
     */
    private $brochure;
}

File upload

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('brochure', FileType::class)
            // ...
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Product::class,
        ));
    }
}

File upload

$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    /** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
    $file = $product->getBrochure();

    // Generate a unique name for the file before saving it
    $fileName = md5(uniqid()).'.'.$file->guessExtension();

    // Move the file to the directory where brochures are stored
    $file->move('/upload_directory', $fileName);

    // Update the 'brochure' property to store the PDF file name
    // instead of its contents
    $product->setBrochure($fileName)
}

Playground

Security

Authentication

# app/config/security.yml
security:
    providers:
        in_memory:
            memory: ~

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        default:
            anonymous: ~

Basic authentication

# app/config/security.yml
security:
    # ...

    firewalls:
        # ...
        default:
            anonymous: false
            http_basic: ~

User provider

# app/config/security.yml
security:
    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: kitten
                        roles: 'ROLE_ADMIN'
                    rogerio:
                        password: 123456
                        roles: 'ROLE_USER'
    # ...

No encoder has been configured for account "Symfony\Component\Security\Core\User\User"

User encoder

# app/config/security.yml
security:
    # ...

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
    # ...

User encoder

# app/config/security.yml
security:
    # ...

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
            cost: 12

$ php bin/console security:encode-password

Changing password


// whatever *your* User object is
$user = new AppBundle\Entity\User();
$plainPassword = 'ryanpass';
$encoder = $this->container->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $plainPassword);

$user->setPassword($encoded);

Access control

# app/config/security.yml
security:
    # ...

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }

Securing controllers

// controller class

public function helloAction($name)
{
    // The second parameter is used to specify on what object the role is tested.
    $this->denyAccessUnlessGranted(
                 'ROLE_ADMIN', 
                  null, 
                  'Unable to access this page!'
    );

   // ...
}

Securing controllers

// controller class

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Security("has_role('ROLE_ADMIN')")
 */
public function helloAction($name)
{
    // ...
}

Securing controllers

// controller class


public function helloAction($name)
{
    $checker = $this->get('security.authorization_checker');

    if (!$checker->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }

    // ...
}

Twig access control

{# twig page #}

{% if is_granted('ROLE_ADMIN') %}
    <a href="...">Delete</a>
{% endif %}

Special attributes

  • IS_AUTHENTICATED_REMEMBERED: All logged in users have this, even if they are logged in because of a "remember me cookie".
  • IS_AUTHENTICATED_FULLY: Users who are logged in.
  • IS_AUTHENTICATED_ANONYMOUSLY: All users (even anonymous ones) have this

Retrieving user

// controller class

public function indexAction()
{
    // via token storage
    $user = $this->get('security.token_storage')->getToken()->getUser();

    // shortcut
    $user = $this->getUser();
}

Retrieving user

{# twig page #}

{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    <p>Username: {{ app.user.username }}</p>
{% endif %}

Hierarchical Roles

# app/config/security.yml

security:
    # ...

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

Login form

# app/config/security.yml
security:
    # ...

    firewalls:
        main:
            anonymous: ~
            form_login:
                login_path: /login
                check_path: /login_check

Security Controller

// src/AppBundle/Controller/SecurityController.php

/** 
 * @Route("/login", name="login") 
 */
public function loginAction(Request $request)
{
    $authenticationUtils = $this->get('security.authentication_utils');

    // get the login error if there is one
    $error = $authenticationUtils->getLastAuthenticationError();

    // last username entered by the user
    $lastUsername = $authenticationUtils->getLastUsername();

    return $this->render('security/login.html.twig', [
            // last username entered by the user
            'last_username' => $lastUsername,
            'error'         => $error,
    ]);
}

Template

{# app/Resources/views/security/login.html.twig #}

{% if error %}
    <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}

<form action="{{ path('login_check') }}" method="post">

    <label for="username">Username:</label>
    <input type="text" id="username" name="_username" 
                              value="{{ last_username }}" />

    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />


    <button type="submit">login</button>
</form>

Update security.yml

# app/config/security.yml

security:
    # ...

    firewalls:
        # order matters! This must be before the ^/ firewall
        login_firewall:
            pattern:   ^/login$
            anonymous: ~

        main:
            pattern:    ^/
            form_login:
                login_path: /login
                check_path: /login_check

    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_ADMIN }

Logging out

# app/config/security.yml
security:
    # ...

    firewalls:
        main:
            # ...
            logout:
                path:   /logout
                target: /

Update routing.yml

# app/config/routing.yml

# ..

login_check:
    path: /login_check

logout:
    path: /logout

Custom User

// AppBundle\Entity\User;

use Symfony\Component\Security\Core\User\UserInterface;


class User implements UserInterface, \Serializable
{
    // ...
    public function serialize()
    {
        return serialize([ 
            $this->id,
            $this->username 
        ]);
    }

    public function unserialize($serialized)
    {
        list(
            $this->id,
            $this->username
        ) = unserialize($serialized);
    }
}

Update security.yml

security:
    # ...

    providers:
        my_provider:
            entity:
                class: AppBundle\Entity\User
                property: username

    encoders:
        AppBundle\Entity\User:
            algorithm: bcrypt
            cost: 12

    firewalls:
        # ...
        main:
            provider: my_provider
            # ...

Playground

The end

Source: The Symfony Book

Made with Slides.com