Nach einer (zu) langen Pause, geht es heute weiter mit dem 4. Teil des Symfony2 Tutorials.
Update
Seit dem letzten Artikel vergangenen Jahres hat sich bei Symfony 2 einiges getan und wurde mittlerweile in einer stabilen Version veröffentlicht. Das wollen wir hier nicht ignorieren und haben das Taskmanager Projekt auf die Symfony Version 2.0.9 geupdated.
Am einfachsten ist es die aktuelle Version des Frameworks von symfony.com herunterzuladen und in einen seperaten Ordner zu entpacken. Aus unserem “alten” Projekt entnehmen wir den /src Ordner und kopieren ihn in das neue Verzeichnis.
Da wir wenige Änderungen am Symfony-Grundsystem vorgenommen haben, müssen wir so nur die Config-Einstellungen ändern und eine Kleinigkeiten in den Models nachziehen, um zu einer lauffähigen Version zu kommen.
Die alten Config Dateien können wir größtenteils ignorieren und diese im neuen Projektordner verwenden.
1. /app/config/routing.yml: Hier übernehmen wir die Routing-Informationen aus dem alten Projekt und kopieren sie in die routing.yml im neuen Verzeichnis. Dadurch haben wir alle Routen eingeschlossen, die wir für unseren Taskmanager brauchen.
2. /app/config/parameters.ini: Die parameters.ini kann nahezu 1:1 kopiert werden, da es sich hier nur um den DB Zugang handelt
3. /app/config/config.yml: Die config.yml kann nahezu unverändert übernommen werden. Wichtig ist, dass wir hier den translator service anschalten (# entfernen). Wer auch PHP als Templating Engine verwenden möchte, kann dies in den templating Einstellungen eintragen (mehr dazu später).
Im Task-Model müssen wir die Reflections über den Attributen ändern (…/src/Scandio/DaylogBundle/Entity/Task.php).
Die ORM-Reflections von Doctrine müssen von @orm:id in @ORM\id geändert werden. Zusätzlich muss noch das Mapping am Anfang der Datei hinzugefügt werden:
<?php
namespace Scandio\DaylogBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Task Model
* @ORM\Entity(repositoryClass="Scandio\DaylogBundle\Entity\TaskRepository")
*/
class Task {
// ...
}
Nach den Änderungen sollte das System wieder lauffähig sein. Wichtig ist auch den Inhalt des /app/cache Ordners zu löschen, um alten Code zu vermeiden.
Allgemeines zu Views
Views werden in Symfony standardmäßig mit Hilfe der Twig Template Engine ausgeliefert. Twig ist wie Symfony ebenfalls von Fabien Potencier kreiert worden und lässt sich ganz im Stile von Symfony2 auch ohne das Framework betreiben.
Dadurch erhält man eine leistungsstarke Template-Engine, die schnell und einfach erlern- und einsatzbar ist.
Wie wir bereits in den vorherigen Teilen gesehen haben, liefert Symfony2 als Return-Werte einer Controller Action idealerweise ein Response-Objekt zurück.
Da wir nun Twig für das Darstellen unserer Views ausgewählt haben, können wir die Controller Funktion $this->render(…) verwenden.
// src/Scandio/DaylogBundle/Controller/TaskController.php
class TaskController extends Controller {
/**
* Index Function to show all tasks
*/
public function indexAction($sort = 'priority')
{
// get Doctrine ORM Entity Manager
$em = $this->getDoctrine()->getEntityManager();
// get the Repository
// location \Scandio\Entity\TaskRepository --> look at the Header Annotation of the Task Entity
$tasks = $em->getRepository('ScandioDaylogBundle:Task')
// custom find Function to take advantage of ordering!
->getAll($sort);
// defining the view vars
$viewVars = array(
'tasks' => $tasks
);
return $this->render('ScandioDaylogBundle:Task:index.html.twig', $viewVars);
}
//...
}
Diese erstellt für uns mit Hilfe des Twig System (besser gesagt, das Templating System, aber dazu später mehr) ein Response Objekt, welches die gerenderte View beinhält.
Standardmäßig werden views im Bundle-Ordner …/Resources/views erstellt. Wenn wir im Controller folgende View spezifizieren:
$this->render('ScandioDaylogBundle:Task:index.html.twig', $viewVars);
wird dies in die Ordnerstruktur
/Scandio/DaylogBundle/Resources/views/Task/index.html.twig
“übersetzt”. Möchte man z.B. eine Controller unabhängige View erstellen, lässt man einfach das “Task” im render-Funktionsaufruf weg: $this->render(‘ScandioDaylogBundle::index.html.twig’, $viewVars); Dies ist z.B. für eine layout.html.twig sehr sinnvoll, die das Gerüst der Seite beinhält.
Das Twig System hat noch einige Kniffe parat: Möchte man eine View in einem Bundle überschreiben, fügt man eine View mit derselben Ordnerstruktur im /app/Resources/ Ordner hinzu.
Bsp. macht das Sinn, wenn man ein externes Bundle wie FOSUser einbindet. Mehr über Twig-Namespaces findet man hier.
View Vererbung
In Twig ist es möglich, views zu vererben. In der Eltern View werden Blöcke definiert, die durch die Kind Views ersetzt bzw. erweitert werden (beides ist möglich!). Um dies zu verdeutlichen hier das Beispiel unserer Tasks index Seite:
…/Resources/views/base.html.twig
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>{%block title%}Scandio Daylog Project!{%endblock%}</title>
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link rel="stylesheet" type="text/css" media="all" href="{{ asset('bundles/scandiodaylog/css/reset.css') }}" />
<link rel="stylesheet" type="text/css" media="all" href="{{ asset('bundles/scandiodaylog/css/text.css') }}" />
<link rel="stylesheet" type="text/css" media="all" href="{{ asset('bundles/scandiodaylog/css/960.css') }}" />
<link href="{{ asset('bundles/scandiodaylog/css/general.css') }}" rel="stylesheet" type="text/css" media="screen, projection" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!-- Adding "maximum-scale=1" fixes the Mobile Safari auto-zoom bug: http://filamentgroup.com/examples/iosScaleBug/ -->
</head>
<body lang="en">
<div id="wrapper" class="container_12">
{% block body %}{%endblock%}
</div>
</body>
</html>
…/Resources/views/Task/index.twig
{% extends 'ScandioDaylogBundle::base.html.twig' %}
{% block body %}
<div class="grid_12">
<h1>
Daylog <a href="http://www.scandio.de">Scandio</a> Symfony2 Tutorial
<a href="{{ path('scandio_daylog_add_prepare') }}">+</a>
</h1>
</div>
<div class="clear"></div>
{% for num, task in tasks %}
<div class="grid_1 {{ num%2==0 ? 'even' : 'odd' }}">
[<a href="{{ path('scandio_daylog_delete', {'taskId': task.id} ) }}" title="delete">x</a>]
[<a href="{{ path('scandio_daylog_edit_prepare', {'taskId': task.id} ) }}" title="edit">e</a>]
{% if task.isDone %}
[<a href="{{ path('scandio_daylog_markAsUndone', {'taskId': task.id} ) }}" title="mark as undone">u</a>]
{% else %}
[<a href="{{ path('scandio_daylog_markAsDone', {'taskId': task.id} ) }}" title="mark as done">d</a>]
{% endif %}
</div>
<div class="grid_1 {{ num%2==0 ? 'even' : 'odd' }}">
{% if task.isDone %}
<strike>{{ task.getPriorityAsString() }}</strike>
{% else %}
{{ task.getPriorityAsString() }}
{% endif %}
</div>
<div class="grid_10 {{ num%2==0 ? 'even' : 'odd' }}">
{% if task.isDone %}
<strike><b>{{ task.title }}</b></strike>
{% else %}
<b>{{ task.title }}</b>
{% endif %}
</div>
{% else %}
<div class="grid_12">
No Tasks found!
</div>
{% endfor %}
<div class="clear"></div>
<div class="grid_12">
sort by:
<a href="{{ path('scandio_daylog_index_sort', {'sort': 'priority'} ) }}" title="order by priority">prio</a> |
<a href="{{ path('scandio_daylog_index_sort', {'sort': 'done'} ) }}" title="order by mark-as-done">done</a>
</div>
{% endblock %}
In der Kind View (index.html.twig) wird zuerst die Eltern View über das keyword “extends” definiert. Als nächstes wird der Block “content” durch dem Block “content” aus der Kind View erstetzt.
Die restlichen Blöcke und View Inhalte aus der Eltern View bleiben erhalten.
Views mit Daten befüllen
Um unserer Anwendung ein wenig Leben einzuhauchen müssen wir die Views mit dynamischen Daten befüllen. Twig nimmt uns hier ebenfalls viel Arbeit ab.
Im allgemeinen werden Daten mit {{ task.name }} ausgegeben. “{{” und “}}” signalisieren twig ein “echo” um Daten auszugeben.
Mit Hilfe des Punktes in “task.name” weißt man Twig an, im Task Objekt (Arrays gleichermaßen) nach der Funktion isName(), getName() oder name() zu suchen und diese auszugeben.
Ist diese nicht vorhanden oder ist der assoziative Array Key nicht definiert, wird ein Fehler geworfen.
Anweisungen werden durch {% if … %} {% else %} {% endif %} definiert. Diese sind nahezu identisch zu den PHP-Äquivalenten. Mehr dazu findet man hier.
Um über Listen zu iterieren, verwendet man eine for-Schleife: {% for num, task in tasks %} {% endfor %} wobei num hier der Iterator-Zähler ist und Task das jeweilge Objekt.
Damit wäre auch das allgemeine Rüstzeug erleutert und man kann nun loslegen und Views erstellen.
Alternativen zu Twig
Wie bereits oben angesprochen lässt sich Symfony2 auch ganz ohne Twig verwenden. Wem die Template-Enginge zu umständlich ist kann auch ganz normales PHP verwenden.
Hierzu muss nur in der config unter “templating: { engines: ['twig'] }” php hinzugefügt werden: “templating: { engines: ['twig', 'php'] }”.
Nun kann man auch views mit der Endung .html.php rendern. Dies ist besonders interessant wenn man verschiedene Formate für die gleiche Action anbieten möchte. Näheres dazu in der Dokumentation.
Fazit
Twig ist ein sehr mächtiges Werkzeug für professionelle Webseitengestaltung. Die Trennung von Programmcode und Views wird durch Twig sehr stark unterstützt.
Man hat größere Schwierigkeiten Programmcode in Twig zu integrieren, als diesen in die passende Models oder Controller auszulagern. Das erhöht die Wiederverwendbarkeit und Lesbarkeit von Code.