Symfony 2 “from scratch” bootstrappen

… machen wir heute mal, weil die Sandbox komisch ist mit den vielen (ärm – 2) redundanten /vendor und /wasweißich-Verzeichnissen.

Also, bauen wir die Sandbox mal nach:

Wir brauchen: Ein halbwegs aktuelles PHP 5.3.x, MySQL 5.x, Apache 2 und git.

Dann erstmal den aktuellen Symfony2-Master ziehen…

$ git clone https://github.com/fabpot/symfony.git symfony2test

… und die Vendor-Scripte installieren:

$ cd symfony2test
$ sh ./install_vendors.sh

Nun sollte sich folgendes Verzeichnislayout ergeben haben:

drwxr-xr-x  6 joshi joshi 4,0K 2010-11-30 21:19 .
drwxr-xr-x 22 joshi joshi 4,0K 2010-11-27 16:34 ..
-rw-rw-rw-  1 joshi joshi  998 2010-11-30 21:10 autoload.php.dist
drwxr-xr-x  8 joshi joshi 4,0K 2010-11-30 21:19 .git
-rw-r--r--  1 joshi joshi   34 2010-11-30 15:49 .gitignore
-rwxrwxrwx  1 joshi joshi  840 2010-11-30 21:10 install_vendors.sh
-rw-rw-rw-  1 joshi joshi 1,1K 2010-11-30 21:10 LICENSE
-rw-rw-rw-  1 joshi joshi 1,1K 2010-11-30 21:10 phpunit.xml.dist
-rw-rw-rw-  1 joshi joshi 1,2K 2010-11-30 21:10 README
drwxrwxrwx  3 joshi joshi 4,0K 2010-11-30 21:10 src
drwxrwxrwx  3 joshi joshi 4,0K 2010-11-30 21:10 tests
-rwxrwxrwx  1 joshi joshi  541 2010-11-30 21:10 update_vendors.sh
drwxrwxrwx 11 joshi joshi 4,0K 2010-11-30 21:13 vendor

Die Vendor-Scripts liegen zunächst mal im Projekt-Root-Verzeichnis; Dort haben sie aber nichts verloren:

$ mv vendor/ src/

Nun haben wir alles, um eine Symfony-App zu erstellen – das funktioniert momentan aber noch ausschließlich in Handarbeit:

$ mkdir -p web app/cache app/logs app/views app/config  src/Bundle src/Application/HelloBundle/Controller src/Application/HelloBundle/Resources/config src/Application/HelloBundle/Resources/views src/Application/HelloBundle/Resources/views/Hello src/Application/HelloBundle/Entity

Anmerkung: Diese wilde Verzeichnisstruktur entspricht 1:1 der Sandbox. Sicherlich steht das endgültige Verzeichnis-Layout eines typischen Symfony2-Projekts noch nicht fest – auch gibt es ja, wie bereits erwähnt, (noch) keine globale Executable samt symfony:generate-project Task. Ein Ziel von Symfony2 ist, die Modularisierung und Entkopplung der einzelnen Framework-Komponenenten (genannt “Bundles”) zu verfeinern. Ähnliche Konzepte kennt man aus der Java-Welt, aber auch bspw. aus CMF wie Silverstripe.

So ist der aktuelle Dev-Branch also eher als lose Sammlung einzelner “Bundles” zu verstehen und nicht als Full-Stack-Framework (Die Menge aller Bundles + Task-Bundle wird aber wieder genau das sein). Am ehesten vielleicht vergleichbar mit dem Aufbau des Zend-Frameworks – wobei das wohl fast als Negativ-Beispiel für “fake-loose-coupling” herhalten kann. Symfony2-Bundles sind “echt” decoupled und erwarten als einzige Abhängigkeit einen PHP-Autoloader.

“Bundles” sind die “Plugins” aus Symfony 1.x, mit dem Unterschied, dass das Core-Framework selbst (“Kernel”) wiederum auch ein Bundle ist.

Kurz gesagt: Das Verzeichnislayout, das in Symfony1.x zwar auch mehr oder weniger flexibel war, doch nur unter Schmerzen tiefgreifend veränderlich war, ist nun erstmal völlig variabel. Da ich mich aber an Fabians vorausschauender best-practice orientieren möchte, sieht “mein” Verzeichnislayout jetzt eben zufälligerweise aus wie das der Symfony2-Sandbox:

app/
  cache/
  config/
  logs/
  views/
autoload.php.dist
install_vendors.sh
LICENSE
phpunit.xml.dist
README
src/
  Application/
    HelloBundle/
      Controller/
      Entity/
      Resources/
        config/
        views/
  Bundle/
  Symfony/
  vendor/
    ...
tests/
update_vendors.sh
web/

Das Verzeichnis /web ist unser Webroot, es sollte nun also ein VirtualHost angelegt werden, der auf dieses zeigt:

<VirtualHost *:80>
  SetEnv APP_ENV dev
  ServerName symfony2.int
  DocumentRoot /var/www/symfony2test/web
</VirtualHost>

Nun können wir damit beginnen, unsere Anwendungsdateien zu erstellen, indem wir einen Frontcontroller anlegen:

web/index.php

<?php
require_once __DIR__.'/../app/AppKernel.php';
use SymfonyComponentHttpFoundationRequest;
$kernel = new AppKernel('prod', false);
$kernel->handle(new Request())->send();

Zunächst holen wir uns den für unsere Anwendung spezifischen Kernel, instanziieren und bitten ihn, einen Http-Request zu bearbeiten.

Damit das Routing funktioniert, benötigen wir noch eine .htaccess-Datei (oder eine entsprechende mod_rewrite-Regel in der VirtualHost-Konfiguration):

web/.htaccess

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

Dann erzeugen wir unseren Kernel und eine Cache-Definition in app/:

app/AppKernel.php

<?php
require_once __DIR__.'/../src/autoload.php';

use SymfonyComponentHttpKernelKernel;
use SymfonyComponentDependencyInjectionLoaderLoaderInterface;

class AppKernel extends Kernel
{
    public function registerRootDir()
    {
        return __DIR__;
    }

    public function registerBundles()
    {
        $bundles = array(
            new SymfonyBundleFrameworkBundleFrameworkBundle(),
            new SymfonyBundleTwigBundleTwigBundle(),

            // enable third-party bundles
            new SymfonyBundleZendBundleZendBundle(),
            new SymfonyBundleSwiftmailerBundleSwiftmailerBundle(),
            new SymfonyBundleDoctrineBundleDoctrineBundle(),
            
            // register your bundles
            new ApplicationHelloBundleHelloBundle(),
        );

        if ($this->isDebug()) {
            $bundles[] = new SymfonyBundleWebProfilerBundleWebProfilerBundle();
        }

        return $bundles;
    }

    public function registerBundleDirs()
    {
        return array(
            'Application'     => __DIR__.'/../src/Application',
            'Bundle'          => __DIR__.'/../src/Bundle',
            'Symfony\Bundle' => __DIR__.'/../src/Symfony/Bundle',
        );
    }

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        // use YAML for configuration
        $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
    }
}

Interessant sind die beiden zu implementierenden Methoden registerBundles() und registerBundleDirs(). RegisterBundles() teilt dem Kernel mit, welche Bundles in der Anwendung aktiv sind und verwendet werden – vergleichbar mit enablePlugins() der projectConfiguration.class.php eines beliebigen Symfony 1.x Projekts. registerBundleDirs() mappt wiederum die einzelnen Bundle-Namespaces auf ihre Pfade. Dies ist eigentlich ein wenig doppelt-gemoppelt, verwenden wir doch einen SPL-Autoloader, um unsere Namespaces zu registrieren. Allerdings benötigt der Kernel noch einmal explizit ein Namespace-Bundle-Pfad-Mapping, um bspw. Template-Namespaces korrekt aufzulösen. Stimmt bspw. der Pfad zum SymfonyBundle-Namespace nicht, läuft zwar die Anwendung, doch die Template-Engine kann die Pfade zu bspw. den Assets (Bildern und CSS-Stylesheets) des WebProfilerBundles nicht auflösen. Dies hätte den gleichen Effekt, als hätte ich vergessen, das Assets-Verzeichnis web/sf in einer Symfony 1.x-Anwendung zu referenzieren.

Vorsicht: Die Sandbox und Beispielscripts verwenden hier teils falsche Pfade: So zeigt der Namespace-Pfad der Symfony-Bundles zunächst auf src/vendor/symfony/src/Symfony/Bundle statt auf src/Symfony/Bundle. Das funktioniert in dem Falle auch, weil es eben zwei Symfony-Installationen in der Sandbox gibt (einmal die via install_vendors.sh und die, die man sich selbst gezogen hat). Man sollte sich immerhin für eine entscheiden und seine Projekt- und Arbeitsdateien entsprechend aufräumen. Aber dafür machen wir’s hier ja auch “from scratch”.

Jede Symfony2-Anwendung benötigt zudem eine konkrete, anwendungsspezifische Cache-Implementierung:

app/AppCache.php

<?php
require_once __DIR__.'/AppKernel.php';
use SymfonyBundleFrameworkBundleCacheCache;
class AppCache extends Cache
{
}

Nun können wir uns an die Konfiguration begeben. In der Datei app/AppKernel.php wird bestimmt, wie und wo konfiguriert wird:

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        // use YAML for configuration
        $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
    }

Demnach müssen wir für jedes Environment eine config_%env%.yml-Datei erstellen. Dadurch, dass das Symfony2 Configuration-Framework explizite Vererbung (besser: imports) untestützt, reicht eine “globale” config.yml und entsprechend angepasste config_%env%.yml-Dateien:

app/config/config.yml

app.config:
    charset:       UTF-8
    error_handler: null
    csrf_secret:   xxxxxxxxxx
    router:        { resource: "%kernel.root_dir%/config/routing.yml" }
    validation:    { enabled: true, annotations: true }
    templating:    {} #assets_version: SomeVersionScheme
    session:
        default_locale: en
        lifetime:       3600
        auto_start:     true

# Twig Configuration
twig.config:
    debug:            %kernel.debug%
    strict_variables: %kernel.debug%

Dann erstellen wir die Konfiguration für das development environment:

app/config/config_dev.yml

imports:
    - { resource: config.yml }

app.config:
    router:   { resource: "%kernel.root_dir%/config/routing_dev.yml" }
    profiler: { only_exceptions: false }

webprofiler.config:
    toolbar: true
    intercept_redirects: true

zend.config:
    logger:
        priority: debug
        path:     %kernel.logs_dir%/%kernel.environment%.log

In Z. 1 wird config.yml als “gobale” Konfiguration importiert. Ansonsten alles mehr oder weniger selbsterklärend, die einzige Auffälligkeit ist, dass das Routing hier nun explizit konfiguriert wird (siehe Z. 5) – im Gegensatz zu Symfony 1.x-Projekten.

Anmerkung: Falls sich jemand wundern sollte, wo und wie diese Fülle an Optionen dokumentiert sind: Hier hilft nur in die jeweilige Service-Definition des DI-Containers zu schauen. Das mag zu Beginn sehr verwirren, doch die einheitliche Bundle-Struktur hilft da wiederum bei der Navigation. Bundle-Konfigurationen findet man ausschließlich im Verzeichnis Resources/ eines Bundles.

Zurück zur Routing-Configuration, die nun angelegt werden muss:

app/config/routing.yml

homepage:
    pattern:  /
    defaults: { _controller: FrameworkBundle:Default:index }

hello:
    resource: HelloBundle/Resources/config/routing.yml

app/config/routing_dev.yml

_main:
    resource: routing.yml

_profiler:
    resource: WebProfilerBundle/Resources/config/routing/profiler.xml
    prefix:   /_profiler

Auch jetzt wurden zwei Routing-Konfigurationen (global und dev), beide werden transparent in config.yml bzw. config_dev.yml referenziert. Die routing_dev.yml ist ausschließlich da um die Route für das WebProfilerBundle zu registrieren.

Die Route “hello” in Z. 5 der routing.yml referenziert wiederum eine Bundle-spezifische Route, abzulegen im HelloBundle:

src/Application/HelloBundle/Resources/config/routing.yml:

hello:
    pattern:  /hello/:name
    defaults: { _controller: HelloBundle:Hello:index }

Somit haben wir fast alles zusammen, um eine Anwendung zum Laufen zu bringen – Front Controller, Kernel-Konfiguration, Cache, Routing – nur die konkrete Implementierung unserer Anwendung fehlt noch sowie ein winziges Detail, der Autoloader. Dieser wird ebenfalls durch unseren Kernel angeschmissen (siehe Z. 1 app/AppKernel.php). Erwartet wird eine flache autoload.php-Datei in src/ – ob sie dort korrekt gelagert ist, oder eher in src/Application liegen sollte oder gar im Projekt root – das kann sich jeder selbst zusammenreimen.

src/autoload.php:

<?php
require_once __DIR__ . '/Symfony/Component/HttpFoundation/UniversalClassLoader.php';

use SymfonyComponentHttpFoundationUniversalClassLoader;

$vendorDir = __DIR__ . '/vendor';

$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
    'Symfony'                        => __DIR__,
    'Application'                    => __DIR__,
    'Bundle'                         => __DIR__,
    'Doctrine\Common\DataFixtures' => $vendorDir.'/doctrine-data-fixtures/lib',
    'Doctrine\Common'               => $vendorDir.'/doctrine-common/lib',
    'Doctrine\DBAL\Migrations'     => $vendorDir.'/doctrine-migrations/lib',
    'Doctrine\ODM\MongoDB'         => $vendorDir.'/doctrine-mongodb/lib',
    'Doctrine\DBAL'                 => $vendorDir.'/doctrine-dbal/lib',
    'Doctrine'                       => $vendorDir.'/doctrine/lib',
    'Zend'                           => $vendorDir.'/zend/library',
));
$loader->registerPrefixes(array(
    'Swift_' => $vendorDir.'/swiftmailer/lib/classes',
    'Twig_'  => $vendorDir.'/twig/lib',
));
$loader->register();

In autoload.php verwenden wir keine PHP-nativen Autoload-Mechanismen noch den SPL-Autoload-Wrapper direkt, sondern eine weitere Symfony2 Framework-Komponente: Den UniversalClassLoader. Dieser Loader kann Namespaces auflösen, lässt sich aber ebenfalls auf die “alte”, unter PHP >= 5.2 gängige Zend bzw. Pear-Notation mappen, was das obige Codesnippet aufzeigt. Intern baut der UniversalClassLoader natürlich auf den entsprechenden SPL-Funktionen auf.

Vorsicht: Der Code entspricht wieder nicht dem originalen Sandbox-Code (Der Namespace-Pfad für die Symfony-Komponenten ist wieder src/ statt src/vendor/symfony/src).

Wir haben weiter oben Routen definiert, die nun entsprechende Endpunkte im HelloBundle brauchen. Wir implementieren dazu eine Bundle-Definition und einen HelloController:

src/Application/HelloBundle/HelloBundle.php

<?php

namespace ApplicationHelloBundle;

use SymfonyComponentHttpKernelBundleBundle;

class HelloBundle extends Bundle
{
}

src/Application/HelloBundle/Controller:

<?php

namespace ApplicationHelloBundleController;

use SymfonyBundleFrameworkBundleControllerController;

class HelloController extends Controller
{
    public function indexAction($name)
    {
        return $this->render('HelloBundle:Hello:index.twig', array('name' => $name));

        // render a PHP template instead
        // return $this->render('HelloBundle:Hello:index.php', array('name' => $name));
    }
}

und die zugehörige View:

{% extends "HelloBundle::layout.twig" %}

{% block content %}
    Hello {{ name }}!
{% endblock %}

Nun müssen wir nur noch die im Template referenzierte Layout-Datei anlegen:

src/Application/HelloBundle/Resources/views/layout.twig:

{% extends "::layout.twig" %}

{% block body %}
    <h1>Hello Application</h1>

    {% block content %}{% endblock %}
{% endblock %}

Und wiederum die in diesem Bundle-spezifischen Layout-Template referenzierte “globale” Layout-Datei – stilsicher mit HTML5-Doctype:

app/views/layout.twig:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>{% block title %}Hello Application{% endblock %}</title>
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>

Das wars. Man sollte nun im Browser zwei Seiten sehen können, einmal die Standard-Projekt-Route “/” auf FrameworkBundle:Default:index gemapped und die Route /hello/:name auf HelloBundle:Hello:index gemapped. Zu beachten ist hierbei das Namespace-Mapping auf die Pfade, die im AppKernel unter registerBundleDirs() angegeben wurden – zum besseren Verständis der Namensraumauflösung der Routen sowie der Templates.

Abschließend kann man sich noch eine Symfony-Konsolenanwendung nach /app legen:

app/console

#!/usr/bin/env php
<?php
require_once __DIR__.'/AppKernel.php';
use SymfonyBundleFrameworkBundleConsoleApplication;
$kernel = new AppKernel('dev', true);
$application = new Application($kernel);
$application->run();

nach einem

$ chmod + x app/console

kann man nun via ./app/console folgende Ausgabe bewundern und sich mit den ersten Symfony-Bundle-Tasks vertraut machen:

$ app/console
Symfony version 2.0.0-DEV - app

Usage:
  [options] command [arguments]

Options:
  --help           -h Display this help message.
  --quiet          -q Do not output any message.
  --verbose        -v Increase verbosity of messages.
  --version        -V Display this program version.
  --ansi           -a Force ANSI output.
  --no-interaction -n Do not ask any interactive question.
  --shell          -s Launch the shell.

Available commands:
  help                         Displays help for a command (?)
  list                         Lists commands
assets
  :install                     
doctrine
  :ensure-production-settings  Verify that Doctrine is properly configured for a production environment.
doctrine:cache
  :clear-metadata              Clear all metadata cache for a entity manager.
  :clear-query                 Clear all query cache for a entity manager.
  :clear-result                Clear result cache for a entity manager.
doctrine:data
  :load                        Load data fixtures to your database.
doctrine:database
  :create                      Create the configured databases.
  :drop                        Drop the configured databases.
doctrine:generate
  :entities                    Generate entity classes and method stubs from your mapping information.
  :entity                      Generate a new Doctrine entity inside a bundle.
  :proxies                     Generates proxy classes for entity classes.
  :repositories                Generate repository classes from your mapping information.
doctrine:mapping
  :convert                     Convert mapping information between supported formats.
  :convert-d1-schema           Convert a Doctrine 1 schema to Doctrine 2 mapping files.
  :import                      Import mapping information from an existing database.
doctrine:query
  :dql                         Executes arbitrary DQL directly from the command line.
  :sql                         Executes arbitrary SQL directly from the command line.
doctrine:schema
  :create                      Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output.
  :drop                        Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output.
  :update                      Processes the schema and either update the database schema of EntityManager Storage Connection or generate the SQL output.
init
  :bundle                      
router
  :debug                       Displays current routes for an application
  :dump-apache                 Dumps all routes as Apache rewrite rules

7 Replies to “Symfony 2 “from scratch” bootstrappen”

  1. Netter Artikel. Werde mir gleich ein Symfony2 nach dieser Anleitung aufsetzen. Danke erstmal für die Mühe.

  2. Danke für den Artikel, ich habe dies soeben mal durchgepsielt, allerdings bekomme ich einen 404 beim laden der hello/Stefan Seite, und ich komme nicht weiter, eventuell, kannst du mir dort einen Rat geben?

    Habe alle Schritte komplett so befolgt. Wenn ich die Sandbox nehme, funktioniert’s. :(

  3. Sehr schöne Anleitung! Ich habe sie einmal komplett durchgearbeitet.

    Dabei sind mir allerdings einige Unstimmigkeiten aufgefallen:

    1) ‘prod’ ist fest eingestellt

    Einerseits steht in der Apache-Konfiguration “SetEnv APP_ENV dev”. Andererseits ist in der index.php der Wert ‘prod’ fest eingestellt, obwohl gar keine config_prod.yml angelegt wird (sondern nur config_dev.yml).

    Die web/index.php sollte stattdessen die APP_ENV-Umbegungsvariable auswerten, etwa so:

    handle(new Request())->send();

    2) Fehlender Dateiname im Hello-Controller

    Den Hello-Controller hast du übertitelt mit:

    “src/Application/HelloBundle/Controller”

    Besser wäre es, wenn Du den Dateinamen mit nennen würdest:

    “src/Application/HelloBundle/Controller/HelloController.php”

    3) Fehlender Dateipfad im Hello-View

    Es wäre praktisch, wenn Du dem Leser den Pfad zur Datei verrätst, in die die Hello-View hinein gehört. :-)

    “src/Application/HelloBundle/Resources/views/Hello/index.twig”

    4) WebProfilerBundle wird nicht geladen

    Einerseits legt die config_dev.yml fest, dass stets der Web-Profiler geladen werden soll. Jedoch wird das WebProfilerBundle niemals geladen, daher läuft die Seite nicht.

    Der Grund ist, dass die AppKernel.php das WebProfilerBundle nur im Debug-Modus lädt. Der Debug-Modus ist aber niemals aktiv, da er in der index.php fest auf “false” gesetzt ist.

    Die web/index.php sollte daher nochmals verbessert werden:

    handle(new Request())->send();

  4. Hey, Danke für die Anmerkungen, so viel Feedback freut mich natürlich.

    Bzgl. irgenwelcher Fragen zur Funktionalität: Das ganze bezieht sich natürlich auf einen jetzt bereits veralteten Snapshot ausm GIT-Repo – das ganze lief selbst zum Zeitpunkt der Erstellung nur mit einigen Hotfixes im einen oder anderen Bundle. Sprich ich stand selbst erstmal davor und hab mir die Haare ausgerissen, weil’s out of the box nicht funktioniert, bis ich es eindeutig auf Regressionbugs in den Framework-Sourcen selbst zurückführen konnte.

    Es kann also sein, dass Teile aus dem Tutorial gar nicht mehr gültig oder gar falsch sind, entweder weil ich Mist geschrieben hab, oder weil sich halt Bugs im Framework befinden oder schlicht die API sich geändert hat – da hilft nur nachbauen und selbst ausprobieren:( Für die Produktion würd’ ichs aber zum jetzigen Zeitpunkt noch nicht empfehlen.

    Viel Spaß!

  5. Eigentlich sehr gut, aber mit der Beta4 bzw. Beta5. aus git vom 13. Juni 2011 nicht mehr aktuell. vorallem

    autoload.php

    und

    AppKernel.php

    enthält nicht mehr aktuelle Namespaces / Verzeichnisse:

    use SymfonyComponentClassLoaderUniversalClassLoader;

    use SymfonyComponentConfigLoaderLoaderInterface;

    zend habe ich noch nicht gefunden

Comments are closed.