PHP

PHP & OOP - FrontController und TemplateEngine (PHP Tutorial)

Tutorial erstellt von Andreas W. in PHP 5.x, letzte Änderung am 22.06.2010

In dem nachfolgenden Tutorial möchte ich euch zeigen, wie man einen FrontController und eine einfache TemplateEngine programmiert und nutzt. Am Ende des Tutorials wird ein kleines, dennoch erhellendes Beispiel zu finden sein, das den Einsatz dieser Implementationen in der Praxis demonstriert. Die Design Patterns, denen die beiden Themenbereiche zu Grunde liegen, werde ich dabei ausführlich erläutern.

FrontController und TemplateEngine


Das FrontController und das TemplateView Pattern, die in diesem Tutorial vorgestellt werden sind beides Design Patterns der Präsentationsschicht. Sie dienen also zur Ausgabe von Daten und erleichtern die Kommunikation mit dem Benutzer der Applikation. Diese zwei Aufgaben lassen auch bereits vermuten, dass sich die Präsentationsschicht in zwei Unterkategorien einteilen lässt. Wir sprechen bei Design Patterns, die sich mit der Interaktion mit dem Benutzer beschäftigen von Command Control Pattern (Command Control Schicht) und bei Pattern zur Darstellung von Daten von View Pattern (View Schicht).

Wie auch hier der Name vermuten lässt verarbeitet ein FrontController die von einem Benutzer übermittelten Daten und gehört daher auch zur Command Control Schicht, während sich das TemplateView Pattern mit der Darstellung der vom Server zusammengestellten Daten geht und es daher zur View Schicht gezählt wird.

Der FrontController

Das FrontController Pattern definiert eine Schnittstelle zum Benutzer. Alle Anfragen (engl. Requests), die an eine Webanwendung, welche mit einem FrontController arbeitet, gesendet werden, werden von diesem weiter an einen Controller zur Weiterverarbeitung gereicht. Zudem ist ein FrontController im Stande, vor und nach der Weiterleitung der Anfrage-Daten an die unterschiedlichen Controller, zentrale Aufgaben, wie zum Beispiel die Authentifizierung eines Benutzers oder das Loggen von Zugriffsdaten, zu erledigen.

Anfragen und Antworten des Servers werden vom FrontController in Objekten verpackt. Standard-gemäß handelt es sich bei einer Webapplikation um HTTP-Requests und -Responses. Um die Anwendung jedoch nicht frühzeitig einzuschränken ist es sinnvoll zwei Interfaces Request und Response zu implementieren, von denen später zum Beispiel die Klassen HTTPResponse und HTTPRequest oder AjaxRequest und AjaxResponse abgeleitet werden können.

Jede Request Klasse sollte dabei Methoden implementieren, die es erlauben auf übergebene Parameter zu prüfen, übergebene Parameter auszulesen, eine Liste aller Parameter zu laden und auf die Header-Einträge eines Request zuzugreifen. Es handelt sich also ausschließlich um die Implementierung einfachster CRUD-Operationen.

Dementsprechend sollte eine Response Klasse Methoden zum Setzen eines Parameters, Hinzufügen eines Headers, Setzen des Antwort-Status und Zurückliefern der Antwort implementieren.

Auf Basis dieser Überlegung ist es einfach die folgenden Interfaces zu implementieren.

Code:

<?php

interface Request
{
    public function issetParam($name);
    public function getParam($name);
    public function getParamList();
    public function getHeader($name);
}

?>


Code:

<?php

interface Response
{
    public function write($data);
    public function addHeader($name, $value);
    public function setStatus($status);
    public function flush();
}

?>


Die zu diesen Interfaces gehörigen Klassen zu konstruieren ist denke ich nicht allzu schwer. Man sollte jedoch wissen, dass die Implementierungen für HTTP auf die PHP eigene Variable $_REQUEST ($_GET und $_POST), sowie auf die Header-Variablen zugreift.

Nun müssen wir jedoch noch die Command Klassen definieren, die es uns ermöglichen, die vom Client gesendeten Anfragen zu verarbeiten und die richtigen Antworten zu generieren. Dazu deklarieren wir ein weiteres Interface, dass nur eine Methode voraussetzt, die die gewünschten Operationen ausführt. Wichtig ist dabei, dass die execute()-Methode nur ein Objekt vom Typ Request und Response erwartet, denn sonst müssten wir bei jeder neuen Request bzw. Response Klasse die Angaben dieses Interfaces überarbeiten. Da jede spezialisierte Klasse jedoch das jeweilige Interface einbindet, reicht diese Definition.

Code:

<?php

interface Command
{
    public function execute(Request $req, Response $res);
}

?>


Ein einfaches Beispiel für eine Command Klasse, die dieses Interface einbindet, wäre das folgende.

Code:

<?php

class SquareCommand implements Command
{
    public function execute(Request $req, Response $res)
    {
        if( $req->issetParam('val') )
        {
            $res->write($req->getParam('val')*$req->getParam('val'));
        }

        else
        {
            $res->write("42");
        }
    }
}

?>


Wie man hier sehr schön sieht, extrahiert die Command Klasse die Daten aus dem übergebenen Request Objekt und verarbeitet diese weiter. Nach der Verarbeitung wird das Ergebnis in das Response Objekt geschrieben. Wurden keine Parameter übergeben, so liefert die Klasse die Antwort aller Fragen: 42. Das einfache Quadrieren einer Zahl ist vielleicht nicht das schönste Beispiel, jedoch denke ich, dass das Anliegen, das hinter einem solchen Controller steckt, klar geworden sein sollte.

Nun stellt sich jedoch eine ganz wichtige Frage. Woher weiß der FrontController an welche Command Klasse er die Anfrage weiterdelegieren soll? Auch diese Frage ist relativ leicht zu beantworten. Diese Aufgabe übernimmt eine speziell dafür ausgelegte Klasse - der CommandResolver. Sie findet zum Beispiel anhand von Übergabeparametern heraus, welches Command Objekt benötigt wird, lädt die entsprechende Klasse, erzeugt das benötigte Objekt und liefert es zurück. Wenn man den Typ des benötigten Command Objekts jedoch tatsächlich über zum Beispiel URL-Parameter übergibt, sollte man darauf achten, dass diese sorgfältig überprüft und ggf. durch einen Default-Command ersetzt werden. Ein Interface für die verschiedenen Resolver sieht, wie Ihr wahrscheinlich richtig vermutet, recht trivial aus.

Code:

<?php

interface CommandResolver
{
    public function getCommand(Request $req);
}
?>


Die von diesem Interface geforderte getCommand()-Methode liefert ein Objekt des benötigten Controllers. Dies kann natürlich nur auf Basis der vom Benutzer übermittelten Daten geschehen, weshalb diese Methode auch ein Objekt erwartet, welches das Request Interface implementiert und somit auch vom Typ Request ist.

Momentan haben wir eigentlich alle Klassen beisammen um einen ersten Test zu starten. Jeder, der dies versucht, wird jedoch feststellen, dass sich dies noch als recht umständlich erweist. Um das noch etwas zu erleichtern, kann nun eine Klasse geschrieben werden, die alle anderen Klassen bündelt und koordiniert - der FrontController. Die Aufgaben sind eigentlich klar. Beim Erzeugen eines neuen FrontController Objekts muss diesem ein Objekt eines CommandResolvers übergeben werden, das intern gespeichert wird und später dazu verwandt werden kann, den zu einem Request Objekt zugehörigen Command herauszufinden und auf diesem die execute()-Methode auszuführen. Eine Methode process() übernimmt diese Aufgabe. Sie lädt mit Hilfe des CommandResolver Objekts ein Objekt des benötigten Commands, übergibt die Objekte Response und Request, führt die execute()-Methode des Command aus und ruft zuletzt noch die flush()-Methode des Response-Objekts auf. Die Gliederung des FrontControllers sieht also wie folgt aus:

Code:

<?php

class FrontController
{
    private $resolver;

    public function __construct(CommandResolver $rsv) {}

    public function process(Request $req, Response $res) {}
}



Mit Hilfe dieser Klasse ist der Aufruf des FrontControllers nun auch ein Kinderspiel.

Code:

<?php

$rsv = new MyCommandResolver('path', 'defaultCommand');
$ctrl= new FrontController($resolver);
$req = new HttpRequest();
$res = new HttpResponse();
$ctrl->process($req, $res);

?>


Als erstes instanziert man die CommandResolver Klasse, die in unserem Fall MyCommandResolver heißt, und einen Pfad zu dem Verzeichnis in dem die Command Klassen liegen, sowie den default Command übergeben bekommt. Dieses Objekt wird nun an ein neues Objekt des FrontControllers gereicht. Beim Aufruf der process()-Methode des Controllers müssen dann nur noch die zwei leeren Request und Response Objekte, die kurz zuvor erzeugt wurden, übergeben werden.

Wie ihr seht, ist dieses Pattern sehr sehr umfangreich und auch nicht ganz trivial. Hat man das Pattern jedoch erstmal verstanden, ist es wirklich nicht schwer und kann einem dabei helfen die Applikation in der es eingesetzt wird sauber zu trennen und dadurch eine einfachere Erweiterbarkeit sowie Wartbarkeit zu schaffen.

Eine einfache TemplateEngine

Da wir nun mit Hilfe des FrontControllers Daten anfordern und zurück liefern können, möchten wir diese natürlich grafisch darstellen können. Dazu werden wir nun eine einfache, aber hilfreiche TemplateEngine implementieren. Wie so oft existiert auch ein Design Pattern, dass dem Entwickler einer solchen Engine einen Vorschlag für eine mögliche Implementierung liefert. Dieses Pattern wird im Allgemeinen als Template-View-Pattern bezeichnet. Es kommt auf die ursprüngliche Aufgabe von PHP als Template-Sprache zurück und nutzt daher eben diese Eigenschaften. Verwendet man PHP um ein Template zu schreiben, nutzt man in der Regel die alternative Syntax, die nur wenigen bekannt ist. So ist es nicht nur möglich PHP-Ausgaben direkt in ein HTML-Template einzubinden, sondern auch logische Strukturen und Blöcke in den Templates zu nutzen. Auf diese Weise kann ein Template, wie zum Beispiel das nachfolgende, gebastelt werden.

Code:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de">
    <head>            
        <title>· <?php echo $this->title; ?> ·</title>

        <meta name="Title" content="<?php echo $this->title; ?>" />
        <meta name="Author" content="<?php echo $this->mail; ?>" />
        <meta name="Publisher" content="<?php echo $this->domain; ?>" />
        <meta name="Copyright" content="<?php echo $this->domain; ?>" />
        <meta name="Revisit" content="After 2 days" />
        <meta name="Keywords" content="pseudo, test, titel, homepage, website" />
        <meta name="Description" content="Besuchen Sie meine Seite! Es könnte sich lohnen." />
        <meta name="Abstract" content="Visit my website. It'll be great." />
        <meta name="Robots" content="INDEX,FOLLOW" />
        <meta name="Language" content="Deutsch" />
        <meta name="Distribution" content="global" />
        <meta http-equiv="content-Type" content="text/html;charset=iso-8859-1" />
        
        <link rel="stylesheet" type="text/css" href="design.css"/>    
    </head>
        
    <body>
        <div id="page">
        
            <h1 id="headline"><?php echo $this->title; ?></h1>        
            
            <ul id="navigation">
                <?php foreach($this->navigation as $entry): ?>
                    <li><a href="<?php echo $entry['url']; ?>"><?php echo $entry['name']; ?></a></li>
                <?php endforeach; ?>
            </ul>
            
            <div id="content">
                <?php echo $this->content; ?>
            </div>                    
            
            <p id="footer">
                powered by <?php echo $this->domain; ?> © <?php echo $this->date; ?>                
            </p>
        </div>
    </body>
</html>


Einige werden sich nun fragen, wieso auf die objektinterne Variablen-Referenzierung $this-> zurückgegriffen wird, aber das lässt sich leicht erklären. Wir werden eine Klasse Template implementieren, die sowohl Getter- als auch Setter- sowie eine Methode zum Parsen und eine zum Parsen und Ausgeben eines Templates bereit stellt. Der Grund wieso es zwei Funktionen gibt, die ein Template parsen, ist recht einfach. Oftmals ist es nötig mehrere Templates geschickt zu verschachteln. Dazu müssen jedoch einzelne Teile des Templates vor dem Zusammenfügen bereits fertig verarbeitet und in reiner HTML-Form verfügbar sein. Für die Getter und Setter Methoden greifen wir auf die PHP eigenen "Magischen Methoden" zurück, sodass wir auf eine private Variable mittels $this->var zugreifen können. Dies ist auch der Grund, wieso wir die Variablen in dieser Form ins Template schreiben. Wir laden jedes Template direkt in unsere Klasse, wodurch der Zugriff mit $this-> ermöglicht wird. Aus unseren Überlegungen ergibt sich nun eine recht überschaubare, jedoch gut einsetzbare Klasse.

Code:

<?php

require_once('ViewHelper.php');

/**
* @package core::template
* @class Template
*
* Template class allows filling templates with life.
* Sure this class supports also the nesting of
* different templates. So you can manage superior
* designs and webpages.
*
* @author Andreas Wilhelm & Mauricio Hannika
* @version
* Version 0.1, 13.02.2009<br />
* Version 0.2, 15.08.2009 (Added View-Helper Support)<br />
* Version 0.3, 09.04.2010 (Updated documentation)<br />
*/  
class Template {
   /**
    * @private
    * The path to the template file.
    */
   private $template;

   /**
    * @var Array $vars The template placeholders and their values
    * @access private
    */
   private $vars = array();

   /**
    * @private
    * The instances of all needed helper classes.
    */
   private $helpers = array();

   /**
    * @public
    *
    * Assigns the template that should be used.
    *
    * @param String $template The path to the template.
    *
    * @author Andreas Wilhelm & Mauricio Hannika
    * @version
    * Version 0.1, 13.02.2009<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function __construct($template){
      $this->setFile($template);
   }

   /**
    * @public
    *
    * Assigns the template file.
    *
    * @param String $template The path to the template.
    *
    * @author Andreas Wilhelm & Mauricio Hannika
    * @version
    * Version 0.1, 13.02.2009<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function setFile($template){

      // Check if the template-file exists and is readable ...
      if( !file_exists($template) || !is_readable($template) ) {
         // ... and throw an exception if it does not.
         throw new Exception('Failed to open template.');
      }  

      // Otherwise assign the template path.
      $this->template = $template;
   }

   /**
    * @public
    *
    * Assigns a template variable.
    *
    * @param String $name The name of the template varible.
    * @param String $value The content of the placeholder.
    *
    * @author Andreas Wilhelm & Mauricio Hannika
    * @version
    * Version 0.1, 13.02.2009<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function assign($name, $value) {
      $this->vars[$name] = $value;
   }

   /**
    * @public
    *
    * Parses a template file and writes the
    * result to the response.
    *
    * @param Request $request The Request object-
    * @param Response $response The Response object.
    *
    * @author Andreas Wilhelm & Mauricio Hannika
    * @version
    * Version 0.1, 13.02.2009<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function render(Request $request, Response $response) {

      // Start the buffering, ...
      ob_start();
      
      // ... try to include the template ...
      if( !include_once($this->template) ) {
         throw new Exception('Failed to parse file.');        
      }

      // ... and get the template data.
      $data = ob_get_clean();
        
      // Finally write the template data to the response object.
      $response->write($data);
   }

   /**
    * @public
    *
    * Parses a file and returns it.
    *
    * @param string $path The path to the template file.
    * @return The parsed template.
    *
    * @author Andreas Wilhelm & Mauricio Hannika
    * @version
    * Version 0.1, 13.02.2009<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function parse($path = null) {
      // Start the content buffering, ...
      ob_start();

      // ... include the template and check for result ...
      if( !@include_once($path) ) {
         // ... and throw an exception on failture.
         throw new Exception('Failed to parse template file.');            
      }

      // Finally get the parsed template ...
      $parsed = ob_get_contents();
      
      // ... and clean up the input buffer.
      ob_end_clean();
        
      return $parsed;
   }

   /**
    * @public
    *
    * Renders the whole template unsing
    * the magic __toString method.
    *
    * @return The parsed template.
    *
    * @author Andreas Wilhelm & Mauricio Hannika
    * @version
    * Version 0.1, 13.02.2009<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function __toString() {  
      return $this->parse( $this->template );
   }

   /**
    * @public
    *
    * Returns a property by a given key.
    *
    * @param String $property The name of the property.
    * @return The value of the template property.
    *
    * @author Andreas Wilhelm
    * @version
    * Version 0.1, 13.02.2009<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function __get($property) {

      // Check if property exists ...
      if( isset($this->vars[$property]) ) {
         // ... and return it if it does.
         return $this->vars[$property];
      }

      // Otherwise return null.
      return null;
   }

   /**
    * @public
    *
    * Calls the decalred helper class.
    *
    * @param String $methodName The name of the called method.
    * @param Array $args The arguments that were given to the called method.
    * @return The result of the helper execution.
    *
    * @author Andreas Wilhelm
    * @version
    * Version 0.1, 15.08.2009 (Added View-Helper Support)<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   public function __call($methodName, $args) {

      // Load the needed helper object ...
      $helper = $this->loadViewHelper($methodName);
      
      // ... and execute the requested helper.
      $val = $helper->execute($args);
      
      return $val;
   }

   /**
    * @protected
    *
    * Instances  the called helper class, saves it to the helper
    * array and returns the creates object.
    *
    * @param String $methodName The name of the called method.
    * @param Array $args The arguments that were given to the called method.
    * @return The requested view helper object.
    *
    * @author Andreas Wilhelm
    * @version
    * Version 0.1, 15.08.2009 (Added View-Helper Support)<br />
    * Version 0.2, 09.04.2010 (Updated documentation)<br />
    */
   protected function loadViewHelper($helper) {
      // Format the name of the helper class, ...
      $helper = ucfirst($helper);
      
      // ... check if the helper was already loaded, ...
      if( !isset($this->helpers[$helper]) ) {
         // ... get the name and the corresponding file of the helper class ...
         $class = $helper . 'ViewHelper';
         $namespace  = 'core::template::' . $helper . 'ViewHelper';
            
         // ... and load it.
         if( !import($namespace) ) {
            return false;
         }
            
         // Finally instance the helper class and store the result.
         $this->helpers[$helper] = new $class();
      }
        
      return $this->helpers[$helper];
   }
}
?>


Ich denke die Klasse klärt alle noch offenen Fragen selbst, daher wollen wir uns nun anschauen, wie man das oben gezeigte Template parsen würde.

Code:

<?php
    try
    {
        /* load required classes */
        require_once('Template.php');

        /* create database connection layer */
        $mysqli = new mysqli('DB_HOST', 'DB_USER', 'DB_PWD', 'DB_NAME');

        /* init template object */  
        $tpl = new Template('layout.php');

        /* check if a site was given using the uri */
        if( empty($_GET['site']) )
        {
            /* parse the content */
            $content = $tpl->parse('home.php');
        }

        /* check if site could not be loaded */
        elseif( !isset($_GET['site']) || !file_exists($_GET['site']) || !is_readable($_GET['site']) )
        {
            throw new Exception('404 - Not Found');
        }

        /* everything alright*/
        else
        {
            $site = $_GET['site'];
            $content = $tpl->parse($site);
        }

        /* assign basic values */
        $tpl->title = 'Meine Website';
        $tpl->domain = 'www.meine-website.de';
        $tpl->mail = 'mich.meine@web.de';
        $tpl->navigation = '';
        $tpl->content = $content;
        $tpl->date = date('Y');
        
        /* create select statement for the navigation */
        $select = "SELECT
                *
            FROM
                `navigation`
            ORDER BY
                `navigation`.`name`;";
        
        /* send sql-query */
        if ( $result = $mysqli->query($sql) )
        {        
            /* save result into an array */
            while( $row = $result->fetch_assoc() )
            {
                $navigation .= $row;
            }
        }

        /* no navigation found*/
        else
        {
            $navigation = '<b>Failed to load the navigation.</b>';
        }

        /* print out template */
        echo $tpl;
    }
        
    catch(Exception $e)
    {
        echo $e->getMessage();
    }
?>


Das sieht doch recht einfach aus. Natürlich gibt es hier noch eine Möglichkeit der Erweiterung. Man kann zum Beispiel unsere TemplateEngine um das View-Helper-Pattern erweitern. Dieses Pattern beschreibt, wie man verschiedene Helper-Klassen implementiert und die Template Klasse so erweitert, dass für die Erstellung von Templates weitere nützliche Funktionen zur Verfügung stehen. Man könnte zum Beispiel eine Klasse schreiben, die es ermöglicht PHP-Quellcode farblich hervorzuheben. Möchte man diese nutzen, lädt man mit einer neuen Methode der Template-Klasse (zum Beispiel loadHelper()) ein gerade erzeugtes Objekt der Helper-Klasse in ein Array, dass alle Helper-Objekte enthält. Mit Hilfe der magischen __call()-Methode kann man es nun ermöglichen diese Objekte wie Methoden der Klasse via $this->helper() im HTML-Template aufzurufen. Solche Helper-Klassen können zum Beispiel auch unsere Filter-Klassen sein, die dafür sorgen könnten, dass ein Stadtname immer groß geschrieben wird.

FrontController und TemplateEngine in Kombination

Nun da wir sowohl den FrontController zur Verarbeitung von Benutzeranfragen als auch die TemplateEngine zur grafischen Darstellung der Antworten implementiert haben, wollen wir uns ansehen, wie diese beiden Pattern ineinander greifen. Das nachfolgende Beispiel ist zwar wieder nicht sehr sinnvoll oder hilfreich, dennoch denke ich, dass es verdeutlicht, wie die Zusammenarbeit unserer Implementationen, aber auch der Command Control und der View Schicht funktionieren.

Basteln wir uns zunächst ein einfaches Template square.php, welches das Ergebnis unserer Quadratur schön einrahmen soll.

Code:

<html>
   <head>
      <title>Easy Square</title>
   </head>
   <body>
      <?php if(isset($this->square)): ?>
         <p>The square of <?php echo $this->x; ?> is <?php echo $this->square; ?></p>
      <?php else: ?>
         <p>Please insert x!</p>
      <?php endif; ?>
   </body>
</html>


Nachdem dies erledigt ist, wollen wir nun unsere SquareCommand Klasse so erweitern, dass sie nicht nur die Quadratur der gegebenen Zahl vornimmt, sondern auch eine Instanz der TemplateEngine erzeugt und die Daten zu einer Ausgabe zusammenführt und im Response Objekt zurück gibt.

Code:

class SquareCommand implements Command
{
    public function execute(Request $req, Response $res)
    {
        $tpl = new Template('square.php');
        $tpl->x = $req->getParam('val');

        if( $req->issetParam('val') )
        {
            $tpl->square = $req->getParam('val') * $req->getParam('val');
        }

        else
        {
            $tpl->square  = "42";
        }

        echo $tpl;
    }
}


Ich möchte an dieser Stelle noch kurz darauf hinweisen, dass eine direkete Ausgabe des Ergebnisses durch das Command Objekt via echo oder $tpl->parse() keine wirklich schöne Lösung ist, man sollte hier in einer echten Applikation anders vorgehen.

Abschließend wollen wir nun noch einen beispielhaften Aufruf unseres Controllers anstoßen.

Code:

<?php

$rsv = new MyCommandResolver('/commands', 'SquareCommand');
$ctrl= new FrontController($resolver);
$req = new HttpRequest();
$res = new HttpResponse();
$ctrl->process($req, $res);

?>


Bei einer entsprechenden Implementierung sollte nun der Standard-Command (in unserem Fall SquareCommand) ausgewählt, erzeugt und ausgeführt werden, wodurch die Ausgabe 42, bei einem Aufruf ohne Parameter und mit Parameter x^2 ausgegeben werden sollte.

Fragen und Antworten

Fragen und Antworten findet ihr selbstverständlich bei uns im Forum. Ich möchte euch auch darum bitten, keine Fragen an mich persönlich zu senden, aus dem einfachen Grund, dass andere Menschen dann nichts von den gegebenen Antworten haben. Zudem können euch im Forum auch viele andere Leute, die echt Ahnung haben, helfen. Natürlich freue ich mich über ein kurzes Feedback zu diesem Tutorial. Ich bedanke mich für eure Aufmerksamkeit und hoffe, dass euch mein kurzer Exkurs gefallen hat.

Liebe Grüsse,

Andreas

http://www.avedo.net

>> Allgemeine Fragen oder Probleme mit dem Tutorial? Hier gehts zum Forum!

Impressum / Datenschutzerklärung          © der-Webdesigner.net 2002 - 2010           top ▲