Introduction to Symfony2: Getting Ready for D8 – If you don't have the project running / see this screen, you're not ready to start! :'( – But first, some basics.



Introduction to Symfony2: Getting Ready for D8 – If you don't have the project running / see this screen, you're not ready to start! :'( – But first, some basics.

1 4


sf2-d8-dcla-slides

Introduction to Symfony2: Getting Ready for D8 - DrupalConLA

On Github jmolivas / sf2-d8-dcla-slides

Introduction to Symfony2: Getting Ready for D8

If you don't have the project running / see this screen, you're not ready to start! :'(

Setup instructions: http://bit.ly/dcon-sf-setup

And ask us for help! :)

Our goal is to leave no developer behind.

Milestones as separate branches.

But first, some basics.

What is Symfony

Symfony is a group of decoupled components and other standalone PHP libraries.

Then, based on these components, Symfony is also a full-stack web framework.

http://symfony.com/components

We can define Symfony as a group of decoupled components and other stand alone libraries.

And then based on these components Symfony is a full-stack web framework.

Does anyone knows which aproach is Drupal 8 using ?

Drupal is not using Symfony as a full stack framework.

Drupal is only taking advantage of some specific components.

Composer

Dependency Manager for PHP.

It allows you to declare the dependent libraries your project needs and it will install them in your project for you.

Install Symfony using the installer

Download installer

$ sudo curl -LsS http://symfony.com/installer -o /usr/local/bin/symfony

$ sudo chmod a+x /usr/local/bin/symfony

Download Symfony

$ symfony new sf2.dev 2.6

You do not have to do this, since you already cloned our example repository.

The HTTP Lifecycle

In every web application, in any language, you always have one goal, read and understand the HTTP Request in order to create and return the appropriate HTTP Response.

HTTP Request-Response

Communication on the web is always a two step process:

  • The client sends an HTTP request
  • The server send back an HTTP response

The Symfony Application Flow

PHP Classes and Namespaces

PHP Classes

A class is a blueprint or set of instructions to build a specific type of object.

Product.php

class Product
{
    private $foo = 'bar';
    public function __construct()
    {
		...
    } 
}

A class is a collection of variables and functions working with these variables.

By default, a PHP class is identified by its name, which must be unique within your project.

Referencing a non-namespaced class

This type of class has no namespace. It can be referenced in two different ways.

// sometimes works :(
$product = new Product();

// always works :)
$product = new \Product();

PHP Namespaces

Are a way of encapsulating items. Bringing the ability to organize and group related classes and also to avoid name collision.

src/AppBundle/Entity/Product.php

namespace AppBundle\Entity;

class Product
{
    private $foo = 'bar';
    public function __construct()
    {
		...
    } 
}

Referencing a namespaced class

A namespaced class can be referenced in two different ways.

// reference by its fully-qualified name
$product = new AppBundle\Entity\Product();

// import the namespace to file
use AppBundle\Entity\Product;

$product = new Product();

Symfony 2

Application Directory Structure

sf2.dev/
  app    | The application code and configuration.
  bin    | Executable code callable from the terminal.
  src    | Our code and configuration.
  vendor | The third-party dependencies.
  web    | The web root public directory

Bundle

A Bundle is a directory that contains the files related to a specific feature in our application, including PHP classes, configuration and asset files as Stylesheets, JavaScript and images.

A bundle is similar to a plugin or module in other paltforms. Everything in Symfony is a bundle, including both the core framework functionality and the code written for your application.

Bundle Directory Structure

sf2.dev/src/Acme/DemoBundle/
  Command
  Controller
  DependencyInjection
  EventListener
  Form
  Resources (config, public, views)
  Tests (Controller)
  Twig (Extension)

Remove the AcmeDemoBundle

  • Remove Bundle registration 'app/AppKernel.php'
  • Remove routing reference 'app/config/routing_dev.yml'
  • Remove directories & files 'src/Acme'

http://bit.ly/sf2-Readme

Remove AcmeDemoBundle registration

app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        ...
        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
        ...
-      		$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
        ...

Remove routing reference

app/config/routing_dev.yml

# AcmeDemoBundle routes (to be removed)
_acme_demo:
    resource: "@AcmeDemoBundle/Resources/config/routing.yml"

Remove directories

$ rm -rf src/Acme

Broken site

No route found for "GET /"

404 Not Found - NotFoundHttpException

The AppBundle

  • The 'AppBundle' Directory 'src/AppBundle'
  • The 'AppBundle' Class at 'src/AppBundle'
  • The 'AppBundle' registration at 'AppKernel' Class

The AppBundle Class

src/AppBundle/AppBundle.php

<?php

namespace AppBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AppBundle extends Bundle
{

}

The AppBundle registration

app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
 		...
            new AppBundle\AppBundle(),
        );
     ...

Routing

A route is a map from a URL path to a controller.

We can define a Route as a map from a URL to a controller.

In the route definition is where where we especify the path for a resource and the controller that will be executed.

Now that I have been mentioning the Controller

Controller

A Controller is a PHP method that takes information from the HTTP request and constructs and returns an HTTP response.

A Controller is PHP a method inside a Controller Class that takes information from the HTTP request object and based on the implemented code contructs and returns a proper HTTP response.

Multiple controller methods can exists in a Controller Class.

This mean we can have as example an index, add, edit, update, delete methods

The Base Controller Class

For convenience, 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.

Symfony\Bundle\FrameworkBundle\Controller\Controller

Symfony provides a Controller Base class containing helper methods with the most common tasks you require inside a controller and also give a method to access services from the service container.

Create a Page

  • Create a 'HelloController' Class with an 'index' Action at 'src/AppBundle/Controller' Directory.

  • Create a Route with path to '/hello/{name}' at 'app/config/routing.yml' file.

Create a Controller Class

src/AppBundle/Controller/HelloController.php

namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class HelloController 
{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello: '.$name.'</body></html>');
    }
}

Create a Route

app/config/routing.yml

hello_controller:
    path:     /hello/{name}
    defaults: { _controller: AppBundle:Hello:index }

Controller naming pattern 'bundle:controller:action'

You can also use the fully-qualified path name.

_controller: AppBundle\Controller\HelloController::indexAction

Other type of Responses

// JsonResponse
return new JsonResponse(['key' => 'value']);

// BinaryFileResponse
return new BinaryFileResponse('path/to/file.txt');

// Redirect
return new RedirectResponse('YOUR_PATH');

Throwing Errors

// throw 404 error
throw new NotFoundHttpException('The product does not exist');

// throw 500 error
throw new \Exception('Something went wrong!');

Render a Template

  • Create the 'index.html.twig' Template file at 'app/Resources/views/hello' Directory.

  • Extend 'HelloController' from 'Controller' Class.

  • Return a response rendering a template, using the 'render' method.

Tip! look at 'src/AppBundle/Controller/DefaultController.php' Class.

Create the Template file

app/Resources/views/hello/index.html.twig

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

{% block body %}
    Hello <strong>{{ name }}</strong>
{% endblock %}

Extend Controller

src/AppBundle/Controller/HelloController.php


use Symfony\Bundle\FrameworkBundle\Controller\Controller;
...

class HelloController extends Controller
{
...

Return a response rendering a template

src/AppBundle/Controller/HelloController.php

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

Base Controller helper methods

// Gets a service by id. Return an object
get(string $id)

// Returns a RedirectResponse to the given URL.
redirect(string $url, integer $status = 302)

// Returns a rendered view as string.
renderView(string $view, array $parameters = array())

// Returns a NotFoundHttpException.
createNotFoundException(
  string $message = 'Not Found', 
  Exception $previous = null
)

Advanced route options

blog:
    path:      /blog/{page}
    defaults:  { _controller: AppBundle:Blog:index, page: 1 }
    requirements:
        page:  \d+

blog_show:
    path:      /blog/{slug}.{_format}
    defaults:  { _controller: AppBundle:Blog:show, _format: html }
    requirements:
    	_format:  html|json

Adding HTTP method requirements

contact:
    path:     /contact
    defaults: { _controller: AppBundle:Main:contact }
    methods:  [GET]

contact_process:
    path:     /contact
    defaults: { _controller: AppBundle:Main:contactProcess }
    methods:  [POST]

Generate Entity and CRUD

  • Generate an Entity named 'Product'
  • Generate a CRUD
  • Create Database and Schema

In order to achieve this three tasks, we are going to take advantage of the Symfony Console Component.

Generate entity

$ php app/console doctrine:generate:entity

The Entity Shortcut Name: AppBundle:Product
Configuration Format: annotation

Fields:
-- name: name
-- type: string
-- length: 255

-- name: price
-- type: float

# hit enter wildly (i.e. agree to the default values)

(if you mess up ctrl + c and start again)

Generate CRUD

$ php app/console doctrine:generate:crud

Entity Shorcut name: AppBundle:Product

Generate write actions: yes

# hit enter wildly (i.e. agree to the default values)

(if you mess up ctrl + c and start again)

Create Database and Schema

$ php app/console doctrine:database:create
$ php app/console doctrine:schema:create

Database settings are in config parameters file (app/config/parameters.yml)

Add Products

Open 'http://sf2.dev/product/new' on your browser.

You should use the DoctrineFixturesBundle to load a controlled set of data into a database.

Create a JSON endpoint

  • Copy the 'showJsonAction' method to the *top* of your 'ProductController' Class

http://bit.ly/sf2-d8-dcla-json

Open 'http://sf2.dev/product/{id}.json' on your browser.

Service

PHP object that performs some sort of "useful" task.

PHP object that performs some sort of "useful" task.

Why we should use services ?

Separate logic into reusable classes/objects.

Service container

A Service Container (or dependency injection container) is simply a PHP object that manages the instantiation of services.

The container is the object that contains all the services and takes care of create instances when requested.

Create a ProductSerializer Class

  • Create a 'ProductSerializer' Class and give it a 'serialize' method:

src/AppBundle/ProductSerializer.php

namespace AppBundle;

use AppBundle\Entity\Product;

class ProductSerializer {  
  public function serialize(Product $product) {
    
    $data = [
      // Copy code to turn product object into JSON here
    ];

    return $data;
  }
}

Instantiate the ProductSerializer

  • Instantiate 'ProductSerializer' at 'ProductController' and use it:
use AppBundle\ProductSerializer;

public function showJsonAction($id)
{
    ...

    $productSerializer = new ProductSerializer();
    $data = $productSerializer->serialize($product);

    return new JsonResponse($data);
}

Register ProductSerializer as service

  • Register the 'ProductSerializer' Class as a service
# app/config/services.yml
services:
  appbundle.product_serializer:
    class: AppBundle\ProductSerializer
  • Use the service at 'ProductController'
public function showJsonAction($id) {
    // ...
    $productSerializer = $this->container
        ->get('appbundle.product_serializer');

    $data = $productSerializer->serialize($product);

    return new JsonResponse($data);
}

Dependency Injection

Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields.

Dependency Injection is a way to pass an object dependencies using constructors or methods or defining into fields.

Using Dependency Injection

use Symfony\Bundle\FrameworkBundle\Routing\Router;

class ProductSerializer {
  private $router;
  public function __construct(Router $router) {
    $this->router = $router;
  }    
  public function serialize(Product $product) {
    $data = [
    // ...
      'url' => $this->router->generate('product_show', ['id' => $product->getId()])
    ];
    return $data;
  }
}
# app/config/services.yml
services:
  appbundle.product_serializer:
    class: AppBundle\ProductSerializer
    arguments: ["@router"]

Drupal 8

Create a module

  • Create the directory 'modules/custom/drupalcon'.
  • Create the 'drupalcon.info.yml' file.
  • Create the 'drupalcon.module' file.

drupalcon.info.yml

name: DrupalCon
type: module
description: DrupalCon Training demo
core: 8.x
package: demo

drupalcon.module

/**
 * Implements hook_theme().
 */

  function drupalcon_theme() {
    $theme = [];

    return $theme;
  }

generate:module (interactive mode)

generate:module (inline mode)

$ drupal generate:module 
  --module=DrupalCon 
  --machine-name=drupalcon 
  --module-path=/modules/custom/ 
  --description='DrupalCon Training demo' 
  --core=8.x 
  --package=demo 
  --dependencies='' 
  --no-interaction

Create a Page

  • Create the 'HelloController' Class with a 'hello' method at 'src/Controller' directory

  • Create a Route with path to '/drupalcon/hello/{name}' at 'drupalcon.routing.yml' file

src/Controller/HelloController.php

namespace Drupal\drupalcon\Controller;

use Drupal\Core\Controller\ControllerBase;

class HelloController extends ControllerBase 
{
  public function hello($name) {
    return [
        '#type' => 'markup',
        '#markup' => $this->t('Hello: ' . $name)
    ];
  }
}

drupalcon.routing.yml

drupalcon.hello_controller_hello:
  path: '/drupalcon/hello/{name}'
  defaults:
    _controller: '\Drupal\drupalcon\Controller\HelloController::hello'
    _title: 'drupalcon Title'
  requirements:
    _permission: 'access content'

generate:controller

$ generate:controller 
  --module="drupalcon" 
  --class-name="HelloController" 
  --method-name="hello" 
  --route="/drupalcon/hello/{name}" 
  --no-interaction

Render a Template

  • Create the 'hello.html.twig' template file at 'templates' directory

  • Update the 'drupalcon_theme' function at 'drupalcon.module' file

  • Update the 'hello' method at 'HelloController' class

templates/hello.html.twig

<div class="drupalcon-hello-text">
  <h1>Hello {{ name }}!</h1>
</div>

drupalcon.module

/**
 * Implements hook_help().
 */
function drupalcon_theme() {
  $theme['hello_page'] = [
     'variables' => ['name' => NULL],
     'template' => 'hello',
   ];

   return $theme;
}

src/Controller/HelloController.php

public function hello($name) {
    return [
      '#theme' => 'hello_page',
      '#name' => $name
    ];
}

Create a Block (plugin)

  • Create the 'ExampleBlock' class at 'src/Plugin/Block/' directory.

  • Provide plugin annotation.

  • Make sure class extends 'BlockBase' class.

  • Implement 'build' method.

generate:plugin:block

$ drupal generate:plugin:block 
  --module="drupalcon" 
  --class-name="ExampleBlock" 
  --label="example_block" 
  --plugin-id="example_block" 
  --no-interaction

src/Plugin/Block/ExampleBlock.php

namespace Drupal\drupalcon\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'ExampleBlock' block.
 *
 * @Block(
 *  id = "example_block",
 *  admin_label = @Translation("example_block"),
 * )
 */
class ExampleBlock extends BlockBase {

  public function build() {
    $build = [];
    $build['example_block']['#markup'] = 'Implement ExampleBlock.';

    return $build;
  }

}

Create a Configuration Form

  • Create the 'EventForm' class at 'src/Form/' directory.

  • Make sure class extends 'ConfigFormBase' class.

  • Implement 'getEditableConfigNames, getFormId, buildForm and submitForm ' methods.

  • Add a Route to '/admin/config/drupalcon/event' at 'drupalcon.routing.yml' file.

  • Add a menu link to 'drupalcon.links.menu.yml' file.

generate:form:config

src/Form/EventForm.php

namespace Drupal\drupalcon\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class EventForm extends ConfigFormBase {

  protected function getEditableConfigNames() {
    return [
      'drupalcon.event_config'
    ];
  }

  public function getFormId() {
    return 'event_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('drupalcon.event_config');
    $form['country'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Country'),
      '#description' => $this->t('Enter country'),
      '#default_value' => $config->get('country'),
    );
    $form['city'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('City'),
      '#description' => $this->t('Enter City'),
      '#default_value' => $config->get('city'),
    );
    $form['date'] = array(
      '#type' => 'date',
      '#title' => $this->t('Date'),
      '#description' => $this->t('Enter date'),
      '#default_value' => $config->get('date'),
    );

    return parent::buildForm($form, $form_state);
  }

  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    parent::submitForm($form, $form_state);

    $this->config('drupalcon.event_config')
      ->set('country', $form_state->getValue('country'))
      ->set('city', $form_state->getValue('city'))
      ->set('date', $form_state->getValue('date'))
      ->save();
  }

}

drupalcon.routing.yml

drupalcon.event_form:
  path: '/admin/config/drupalcon/event'
  defaults:
    _form: '\Drupal\drupalcon\Form\EventForm'
    _title: 'EventForm'
  requirements:
    _permission: 'access administration pages'

drupalcon.links.menu.yml

drupalcon.event_form:
  title: 'EventForm'
  description: 'Set Default Event data'
  route_name: drupalcon.event_form
  parent: system.admin_config_system

Create a Service

  • Create the 'Event' class at 'src' directory.

  • Register 'Event' class at 'drupalcon.services.yml' file.

  • Give it a 'getInformation' method.

generate:service

$ drupal generate:service 
  --module="drupalcon" 
  --service-name="drupalcon.event" 
  --class-name="Event" 
  --interface="no" 
  --services="config.factory" 
  --no-interaction

src/Event.php

namespace Drupal\drupalcon;

use Drupal\Core\Config\ConfigFactory;

class Event {

  protected $config_factory;

  public function __construct(ConfigFactory $config_factory) {
    $this->config_factory = $config_factory;
  }
}

Give it a getInformation method

...

  public function getInformation() {
    $config = $this->config_factory->get('drupalcon.event_config');

    return sprintf(
      'DrupalCon is happenig at %s, %s and starts %s',
      $config->get('country'),
      $config->get('city'),
      $config->get('date')
    );
  }

...

drupalcon.services.yml

services:
  drupalcon.event:
    class: Drupal\drupalcon\Event
    arguments: ["@config.factory"]

Use the Event Service

  • Create the 'EventController' Class at 'src/Controller' directory and Inject the 'drupalcon.event' service

  • Create the 'EventBlock' class at 'src/Plugin/Block/' directory and Inject the 'drupalcon.event' service.

generate:controller (service)

$ drupal generate:controller 
  --module="drupalcon" 
  --class-name="EventController" 
  --method-name="drupalcon" 
  --route="/drupalcon" 
  --services="drupalcon.event" 
  --no-interaction

src/Controller/EventController.php

namespace Drupal\drupalcon\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\drupalcon\Event;

class EventController extends ControllerBase implements ContainerInjectionInterface {

  protected $drupalcon_event;

  public function __construct(Event $drupalcon_event) {
    $this->drupalcon_event = $drupalcon_event;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('drupalcon.event')
    );
  }

  public function drupalcon() {
    return [
        '#type' => 'markup',
        '#markup' => $this->drupalcon_event->getInformation()
    ];
  }

}

generate:plugin:block (service)

$ dupal generate:plugin:block 
  --module="drupalcon" 
  --class-name="EventBlock" 
  --label="event_block" 
  --plugin-id="event_block" 
  --services="drupalcon.event" 
  --no-interaction

src/Plugin/Block/EventBlock.php

namespace Drupal\drupalcon\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\drupalcon\Event;

/**
 * Provides a 'EventBlock' block.
 *
 * @Block(
 *  id = "event_block",
 *  admin_label = @Translation("event_block"),
 * )
 */
class EventBlock extends BlockBase implements ContainerFactoryPluginInterface {

  protected $drupalcon_event;

  public function __construct(
        array $configuration,
        $plugin_id,
        $plugin_definition,
        Event $drupalcon_event
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->drupalcon_event = $drupalcon_event;
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('drupalcon.event')
    );
  }

  public function build() {
    $build = [];
    $build['event_block']['#markup'] = $this->drupalcon_event->getInformation();

    return $build;
  }

}

Create a Content Entity

  • Too many steps and files to create/update.

  • Better use Console and ask Tess (@socketwench), you know the Drupal 8 Flag use it.

generate:entity:content

$ drupal generate:entity:content 
  --module="drupalcon" 
  --entity-class="Foo" 
  --entity-name="foo" 
  --no-interaction

Symfony & PHP resources

Drupal 8 Resources

Thanks

Questions & feedback