Einführung in

Symfony 2

Version 2.7 (LTS)

Philipp Marien & Alexander Bogomolov

Was ist Symfony2?

Symfony is a set of reusable PHP components...
... and a High Performance PHP framework for web projects

Wer nutzt Symfony

  • Drupal
  • Composer
  • Piwik
  • Silex
  • Symfony CMF
  • Behat
  • Shopware
  • Doctrine
  • PHPUnit
  • Propel
  • Laravel
  • eZ Publish
  • Joomla!
  • phpBB
  • viele mehr...

Woraus besteht Symfony2?

Components

Symfony Components are a set of decoupled and reusable PHP libraries.

Bridges

A bridge is a set of classes that aims at extending a library into Symfony2.
  • Doctrine
  • Monolog
  • Swiftmailer
  • Twig

Bundles

A bundle is a directory that has a well-defined structure and can host anything from classes to controllers and web resources.
  • Application-specific bundles
  • Reusable bundles

Architektur

Model View Controller (MVC)

I (Fabien Potencier) don't like MVC because that's not how the web works. Symfony2 is an HTTP framework; it is a Request/Response framework.

Application Flow

Symfony2 - Application Flow

Symfony - Verzeichnisstruktur

app/
  config/
    config.yml
    parameters.yml
    routing.yml
    security.yml
  AppKernel.php
bin/
  console
src/
  AppBundle/ # Default Bundle, nur für sehr kleine Anwendungen
  Bundles/ # Eigene Bundles
  Components/ # Eigene Components
  Bridges/ # Eigene Bridges
var/
  logs/
  cache/
vendor/
web/
  app.php
  app_dev.php
composer.json

Bundle - Verzeichnisstruktur

MyBundle/
  Controller/
    MyController.php
  DependencyInjection/
    Configuration.php
    MyExtension.php
  Entity/
    MyEntity.php
  Form/
    MyFormType.php
  Repository/
    MyEntityRepository.php
  Resources/
    config/
      services.yml
      routing.yml
    view/
  Tests/
  MyBundle.php

Konfiguration

  • YAML (globale Konfiguration, Routing)
  • Annotations (Doctrine)
  • XML (Services)
  • PHP (bitte nicht benutzen)

Installation

Vorbereitungen (Symfony 3)

export SENSIOLABS_ENABLE_NEW_DIRECTORY_STRUCTURE=true

... in die Datei ~/.bashrc oder ~/.bash_profile

Composer

composer create-project symfony/framework-standard-edition workshop

Globale Parameter

# app/config/parameters.yml

parameters:
  database_host: 127.0.0.1
  database_port: null
  database_name: symfony
  database_user: root
  database_password: null
  mailer_transport: smtp
  mailer_host: 127.0.0.1
  mailer_user: null
  mailer_password: null
  secret: 808a1411b663ad30dbbb5fab88137f17823dac3b

Aufgabe

  • Installiere Symfony 2 über Composer auf deinem lokalen System
  • Verwende die Symfony 3 Verzeichnisstruktur

Doctrine

Teil 1

Was ist Doctrine?

The core projects are a Object Relational Mapper (ORM) and the Database Abstraction Layer (DBAL) it is built upon.


  • MySQL
  • Oracle
  • Microsoft SQL Server
  • PostgreSQL
  • SAP Sybase SQL Anywhere
  • SQLite
  • Drizzle
Code Datenbank
Klasse (Entität) Tabelle
Objekt Tabellenzeile
Attribut Tabellenspalte
Identität eines Objekts Primärschlüssel
Referenzen auf andere Objekte Fremdschlüssel

Object Relational Mapper

Entitäten

  • eindeutiges Objekt
  • individuelle Informationen
  • Beispiele: Person, Status, Gruppe

Beispiel: User Entität

<?php

namespace AppBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* User
*
* @ORM\Table(name="app_user")
* @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User {

  /**
  * @var integer
  *
  * @ORM\Column(name="id", type="integer")
  * @ORM\Id
  * @ORM\GeneratedValue(strategy="AUTO")
  */
  private $id;

  /**
  * @var string
  *
  * @ORM\Column(name="email", type="string", length=255, unique=true)
  */
  private $email;

  /**
  * @var string
  *
  * @ORM\Column(name="password", type="string", length=255)
  */
  private $password;

  /**
  * @var string
  *
  * @ORM\Column(name="salt", type="string", length=255, nullable=true)
  */
  private $salt;

  /**
  * @var string
  *
  * @ORM\Column(name="first_name", type="string", length=255)
  */
  private $firstName;

  /**
  * @var string
  *
  * @ORM\Column(name="last_name", type="string", length=255)
  */
  private $lastName;

  /**
  * @var ArrayCollection
  *
  * @ORM\OneToMany(targetEntity="AppBundle\Entity\Address", mappedBy="user")
  */
  private $addresses;

  publich function __construct(){
    $this->addresses = new ArrayCollection();
  }
  // Getter & setter
}

Mapping

Ganz einfach mit Annotations...
use Doctrine\ORM\Mapping as ORM;

                /**
                 * @ORM\Table(name="app_user")
                 * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
                 */
                class User {
                  // ...
                }
            

                /**
                 * @ORM\Column(name="id", type="integer")
                 * @ORM\Id
                 * @ORM\GeneratedValue(strategy="AUTO")
                 */
                protected $id;
            

                /**
                 * @ORM\Column(name="email", type="string", length=255, unique=true,nullable=false)
                 */
                protected $email;
            

Ein Nutzer hat mehrere Adressen...

... viele Adressen haben einen Nutzer


AppBundle:Address

                /**
                 * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="addresses")
                 * @ORM\JoinColumn(name="user_id")
                 */
                protected $user;
            
AppBundle:User

                /**
                 * @ORM\OneToMany(targetEntity="AppBundle\Entity\Address", mappedBy="user")
                 */
                protected $addresses;
            

Repositories

... enthalten die Logik um ein oder mehrere Objekte aus der Datenbank zu laden.



Standard-Methoden:

  • find($id)
  • findAll()
  • findOneBy(array $criteria, array $order)
  • findBy(array $criteria, array $order)

UserRepository

extends EntityRepository

<?php

namespace AppBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * UserRepository
 */
class UserRepository extends EntityRepository
{
  public function findAllUserWithDomain($domain) {
    return $this->getEntityManager()->createQuery(
            'SELECT u FROM AppBundle:User u WHERE u.email LIKE :domain ORDER BY u.firstName ASC'
          )
          ->setParameter('domain', '%' . $domain)
          ->getResult();
  }
}

Command Line Tools

bin/console doctrine:database:create
bin/console doctrine:database:drop
bin/console doctrine:schema:validate
bin/console doctrine:schema:create
bin/console doctrine:schema:update

Aufgabe

Teil 1

  • Lege die Entität "User" an
  • Die Entität "User" muss das Symfony "UserInterface" implementieren
  • Lege zu deiner Entität ein Repository an
  • Erstelle die Datenbank mit Doctrine
  • Erstelle das Datenbankschema mit Doctrine

Es geht auch einfacher...

bin/console doctrine:generate:entity

Aufgabe

Teil 2

  • Lege die Entitäten "Book" und "Reservation" an (inkl. Repositories)
  • Verknüpfe die Entitäten mit "OneToMany" und ManyToOne"
  • Update das Datenbankschema mit Doctrine

Security

Security Component
The Security component provides a complete security system for your web application.
It ships with facilities for authenticating using HTTP basic or [...] interactive form login [...], but also allows you to implement your own authentication strategies.
[...] the component provides ways to authorize authenticated users based on their roles, and it contains an advanced ACL system.
Authentifizierung
  • System-Zugang
  • Nutzeridentität
Autorisierung
  • Ressourcen-Zugang
  • Berechtigungen prüfen

Providers

Woher kommen die Nutzer?

providers:
  in_memory:
    memory:
      users:
        ryan:
          password: test56
          roles: 'ROLE_USER'
        admin:
          password: test56
          roles: 'ROLE_ADMIN'

  database:
    entity:
      class: AppBundle\Entity\User
      property: email

  webservice:
    id: my_webservice_manager
# ...

Firewalls

Wie authentifizieren sich die Nutzer?
Dürfen anonyme Nutzer die Seite aufrufen?

firewalls:
  not_secured:
    pattern: ^/test$
    security: false

  anonym:
    anonymous: ~
    form_login:
      login_path: login
      check_path: login_check

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

Encoders

Wie werden die Passwörter der Nutzer verschlüsselt?

encoders:
  Symfony\Component\Security\Core\User\User: plaintext
  AppBundle\Entity\User: sha512

# ...

Aufgabe

  • Konfiguriere deine User-Entität als Provider
  • Konfiguriere einen Passwort-Encoder für deine User-Entität
  • Konfiguriere eine Firewall mit "HTTP Basic Auth"
  • Konfigure einen Memory-Provider mit einem Test-User
  • Konfigure einen Passwort-Encoder für den Test-User

Controller

und

Routing

Routing Component

Beispiel: HelloController

class HelloController extends Controller
{
  public function helloAction()
  {
    return new Response('Hello world!');
  }

  public function helloNameAction($name)
  {
    return new Response('Hello ' . $name . '!');
  }
}

Routing

# app/config/routing.yml
app:
  resource: "@AppBundle/Resources/config/routing.yml"

root:
  path: /
  defaults:
    _controller: FrameworkBundle:Redirect:urlRedirect
    path: /blog
# AppBundle/Resources/config/routing.yml
blog_post_show:
  path:     /blog/{slug}
  methods: GET
  defaults: { _controller: AppBundle:Blog:show }

blog_post_update:
  path:     /blog/update/{id}
  methods: GET|POST
  defaults: { _controller: AppBundle:Blog:update }
  requirements:
    id: \d+

Aufgabe

  • Lege einen Test Controller an
  • Lege eine "helloAction" an
  • Lasse die Action unter der Route "/hello" den String "Hallo Welt" ausgeben
  • Lege eine weitere Action an, die einen String als Parameter erhält und diesen ausgibt

Templates

Templating Component

Engines

  • Twig (default)
  • PHP
  • und alle anderen eigl. auch

Struktur

src/
  MyBundle/
    ...
    Controller/
      TestController.php
    Resources/
      public/
        assets/
      views/
        Test/
          index.html.twig
      layout.html.twig

Twig

Twig is a modern template engine for PHP

Komponenten

  • Tags
  • Filters
  • Functions
  • Tests
  • Operaters

Tags

{% tag parameters %}{% endtag %}

Filters

{{ variable|filter(parameters) }}

Functions

{{ function(parameters) }}

Tests

{% if variable is test %}{% endif %}

Operators

{{ function(parameters) }}

layout.html.twig

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>{% block title %}Welcome!{% endblock %}</title>
  {% block stylesheets %}
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" media="all"/>
  {% endblock %}
  <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}"/>
</head>
<body>
<main class="container">
  {% include '::_navigation.html.twig' %}

  {% block body %}{% endblock %}
</main>
{% block javascripts %}
  <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
{% endblock %}
</body>
</html>

index.html.twig

{% extends 'MyBundle::layout.html.twig' %}

{% block title %}
  {{ title }}
{% endblock %}

{% block body %}
  <h1>Hallo Welt!</h1>
  <p>Lorem impsum...</p>
{% endblock %}

Controller mit Template

class TestController extends Controller
{
  public function indexAction()
  {
    return $this->render('AppBundle:Test:index.html.twig', ['title' => 'Hallo Welt']);
  }

  public function layoutAction()
  {
    return $this->render('AppBundle::layout.html.twig', []);
  }
}

Aufgabe

  • Lege ein Template "layout.html.twig" an
  • Lege ein Template "hello.html.twig" an, welches das vorherige Template erweitert
  • Ändere die Actions der letzten Aufgabe um die Texte im neuen Template ausgeben zu lassen

Doctrine

Teil 2

Entity Manager

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

create


                $user = new User();
                $em->persist($user);
                $em->flush();
            

update


                $user->setFirstName('Test');
                $em->flush();
            

delete


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

Repository

$repository = $this->getDoctrine()->getRepository('AppBundle:User');
 $user = $repository->find($id); 
 $user = $repository->findOneBy(['firstName'=>'Test']); 
 $users = $repository->findAll(); 
 $users = $repository->findBy(['lastName'=>'User'], ['firstName' => 'ASC']); 
 $users = $repository->findByCustomQuery(); 

Aufgabe

  • Lege einen "BookController" an
  • Lege eine "listAction" an, die alle Bücher als Tabelle ausgibt
  • Lege eine "borrowAction" und eine "returnAction" an

Formulare

Form Component

Formular

class ContentType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder
      ->add('title')
      ->add('body')
      ->add('save', 'submit', array('label' => 'Create Content'))
    ;
  }

  public function getName()
  {
    return 'content';
  }

  public function configureOptions(OptionsResolver $resolver)
  {
    $resolver->setDefaults(array(
      'data_class' => 'MyBundle\Entity\Content',
    ));
  }
}

Formular Template

{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

Controller

class DefaultController extends Controller
{
  public function newAction(Request $request)
  {
    $form = $this->createForm(new ContentType(), new Content());

    $form->handleRequest($request);

    if ($form->isValid()) {
      //...
      return $this->redirectToRoute('success');
    }

    return $this->render('default/new.html.twig', array(
      'form' => $form->createView()
    ));
  }
}

Aufgabe

  • Erstelle ein Registrierungsformular für deine User Entität
  • Erstelle ein Update-Formular für deine User Entität
  • Erstelle einen "UserController" mit "registrationAction", "updateAction" und "deleteAction"
  • Erstelle ein Formular für deine Book Entität
  • Erweitere deinen "BookController" mit "createAction", "updateAction", "deleteAction"

Service Container

DependencyInjection Component

Dependency Injection

  • Design Pattern
  • Abhängigkeiten (Dependencies) zentral verwalten
  • Constructor Injection
  • Setter Injection

Was ist ein Service?


            class HelloController extends Controller
            {
              public function sendEmailAction()
              {
                $mailer = new Mailer('sendmail');
                $mailer->send('foo@bar.net', 'Hello World!');
                // ...
              }
            }
        

            class HelloController extends Controller
            {
              public function sendEmailAction()
              {
                $mailer = $this->get('enm_mailer');
                $mailer->send('foo@bar.net', 'Hello World!');
                // ...
              }
            }
        

Service Definition


            
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>

        <service id="enm_mailer" class="EnmHelloBundleMailer">
            <argument>sendmail</argument>
        </service>

    </services>

</container>
            
        

Service Parameter


            
<parameters>

    <parameter key="enm_mailer.transport">sendmail</parameter>

</parameters>

<services>

    <service id="enm_mailer" class="EnmHelloBundleMailer">
        <argument>%enm_mailer.transport%</argument>
    </service>

</services>
            
        

Extension


                class AppExtension extends Extension {
                  public function load(array $config, ContainerBuilder $container)
                  {
                    $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
                    $loader->load('services.xml');
                  }
                }
            

Container Parameter


            # app/config/parameters.yml

            parameters:
              mailer.transport: sendmail
        
$container->hasParameter('mailer.transport');
$container->getParameter('mailer.transport');
$container->setParameter('mailer.transport', 'sendmail');

Service Tags

In the service container, a tag implies that the service is meant to be used for a specific purpose

            
<services>

    <service id="foo.twig.extension" class="EnmHelloBundleExtensionFooExtension" public="false">
        <tag name="twig.extension"/>
    </service>

</services>
            
        

Aufgabe

  • Erstelle einen "MailManager" mit den Constructor-Argumenten "mailer" und "twig"
  • Konfiguriere den MailManager als Service "app.mailer"

Doctrine

Teil 3 - QueryBuilder und DQL

DBAL (DQL)

The Doctrine database abstraction & access layer (DBAL) offers a lightweight and thin runtime layer arounda PDO-like API and a lot of additional, horizontal features like database schema introspection and manipulation through an OO API.

QueryBuilder

<?php

namespace AppBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * UserRepository
 */
class UserRepository extends EntityRepository
{
  public function findAllUserWithDomain($domain) {
    return $this->createQueryBuilder('u')
            ->where('u.email LIKE :domain')
            ->orderBy('u.firstName', 'ASC')
            ->setParameter('domain', '%' . $domain)
            ->getQuery()
            ->getResult();

  }
}

Aufgabe

  • Lege im "ReservationRepository" eine Methode "findNextReservationForBook" an
  • SELECT r.*
    FROM reservation r
    LEFT JOIN book b ON r.book_id = b.id
    LEFT JOIN user u ON r.user_id = u.id
    WHERE b.id = ? AND u.id != ?
    ORDER BY r.registered_at DESC
  • Nutze den QueryBuilder

Events

EventDispatcher Component
The EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them.

Beispiel Szenario

Beim Anlegen eines neuen Benutzers soll eine Bestätigungs-E-Mail an diesen verschickt werden.

Event-Klasse

class UserEvent extends Event {

  protected $user;

  public function __construct(User $user) {
    $this->user = $user;
  }

  public function getUser() {
    return $this->user;
  }
}

Dispatching the Event

class UserController extends Controller {

  public function userSaveAction(Request $request) {
    // [...]
    $doctrine->persist($user);
    $doctrine->flush();

    $dispatcher = $this->get('event_dispatcher');
    $dispatcher->dispatch('myapp.user_insert', new UserEvent($user));
    // [...]
    return ['user' => $user];
  }

}

Event-Listener

namespace MyBundle\EventListener;

class UserEventListener {

  protected $mailer;

  public function __construct(\Swift_Mailer $mailer) {
    $this->mailer = $mailer;
  }

  public function onUserInsert(UserEvent $event) {
    $message = \Swift_Message::newInstance()->setTo($event->getUser()->getEMail());
    // [...]
    $this->mailer->send($message);
  }
}

Listener registrieren


<service id="kernel.listener.user_insert_listener"
         class="MyBundleEventListenerUserEventListener">
    <argument type="service" id="mailer"/>
    <tag name="kernel.event_listener" event="myapp.user_insert" method="onUserInsert"/>
</service>

Aufgabe

  • Lege ein "BookEvent" an
  • Löse in der "borrowAction" und in der "returnAction" vom BookController Events aus
  • Lege einen "BookListener" mit den Methoden "onBookBorrow" und "onBookReturn" an
  • Registriere deinen "BookListener" im ServiceContainer als EventListener

Commands

Console Component
The Console component eases the creation of beautiful and testable command line interfaces.

Ein Beispiel...

bin/console enm:mail marien@eosnewmedia.de "Hallo!"
class MailCommand extends Command
{
  protected function configure()
  {
    $this->setName('enm:mail');
    $this->addArgument('to', InputArgument::REQUIRED, 'Who should receive your email?');
    $this->addArgument('text', InputArgument::REQUIRED, 'Your email text...');
  }

  protected function execute(InputInterface $input, OutputInterface $output)
  {
    $to = $this->getArgument('to');
    $text = $this->getArgument('text');
    // [...]
    $output->writeln('E-Mail has been sent.');
  }
}

Console Helpers

Aufgabe

  • Schreibe ein Command, welches den aktuellen Nutzer eine Buches informiert, wenn ein anderer Nutzer bereits eine Woche oder länger darauf wartet