HMVC – Frameworks in PHP und deren Probleme
Während noch vor einigen Monaten keiner von HMVC sprach, wird das Thema in letzter Zeit immer populärer. Im Java-Bereich gibt es das Konzept schon länger, in PHP scheint es relativ neu zu sein. Zumindest verglichen mit dem simplen MVC-Pattern. Für alle, die sich jetzt fragen, was HMVC überhaupt ist, folgt eine kleine Einführung in die Thematik. Die anderen überspringen den nächsten Abschnitt einfach.
HMVC ist…
sozusagen die Erweiterung von MVC. Wer nicht weiß, was MVC ist, der liest sich am besten mal bei Wikipedia ein. Der Artikel würde zu lang werden, wenn ich hier alles erklären würde. Wie gesagt, HMVC ist die Erweiterung von MVC. Das H steht für Hierarchical, während MVC wie beim normalen MVC für Model View Controller steht. Der Unterschied zw. MVC und HMVC ist, dass es bei HMVC mehrere MVC-Pakete (ich nenne sie einfach mal Module) gibt. Diese können beliebig verschachtelt werden und so ist es möglich, eine komplette Seite ganz einfach zusammenzusetzen. Mal ein Beispiel:
Eine Seite hat folgende Bestandteile (Module)
- Menü
- Loginbox
- Chatbox
- Contentbereich (wo abhängig von der aufgerufenen Seite News, Gästebuch oder Forum angezeigt wird)
Probleme bei der Umsetzung mit „normalem MVC“
Würde man jetzt das normale MVC-Pattern implementieren, hätte man ein Problem. Denn wie realisiert man, dass jedes Modul eigene Logik enthalten kann? Am einfachsten geht dies mit sogenannten Subcontrollern. So hat es bis vor kurzem auch noch in meinem eigenen MVC-Framework funktioniert. Es werden für Dinge, die immer geladen werden, SubController (bei mir permanente SubController) implementiert. Diese werden immer ausgeführt, wenn ein Request abgearbeitet wird. So kann man auf jeder Seite z.b. das passende Menü laden oder abhängig davon, ob der User eingeloggt ist oder nicht, die Loginbox zeigen.
Der Nachteil davon ist, dass durch permanente SubController auf jeder Seite diese Dinge geladen und angezeigt werden. Natürlich könnte man einfach im Hauptcontroller sagen, dass er neue SubController nachladen soll. Aber irgendwie gefällt mir keine von diesen Ideen so richtig.
Die Lösung des APF
Das APF (Adventure PHP Framework) nutzt ein für die PHP-Welt völlig neues Konzept. Es arbeitet mit sogenannten Taglibs. Das sind XML-Tags, die verschiedene Funktionalitäten haben. z.B. kann man damit weitere Templates einbinden oder auch Formulare inkl. Validatoren „ganz einfach“ in ein Template einbetten. Es gibt Templates, die einen zugehörigen Controller haben. Dieser Controller wird vom Template selbst bestimmt. Das hat einige Vorteile, aber auch Nachteile:
- Es gibt keine Controller ohne Templates. Dadurch dass der sogenannte PageController über die Templates bestimmt, welche Controller ausgeführt werden, muss immer ein Template vorhanden sein, um Logik auszuführen. Somit muss jeder Controller eine Ausgabe erzeugen. Was ist aber, wenn ein Controller in einer Aktion einfach nur Daten abspeichern und per header() weiterleiten soll? Richtig, das endet in Pfusch!
- Man muss eine neue Sprache erlernen. Das APF nutzt eine ganz eigene Sprache in den Templates. Für Anfänger ist dies sehr unübersichtlich und schwer zu erlernen. Oft wird mehr Code benötigt als wenn man es mit normalem HTML schreiben würde. Finde ich persönlich sehr nervig.
- Man kann nur HTML ausgeben, kein XML oder JSON. Zumindest wäre mir kein Weg bekannt. Dadurch dass die Templates bestimmen, welche Controller ausgeführt werden, hat der Controller keine Wahl zwischen den Templates. So sollte es aber eigentlich sein.
- Je nach Zustand des Users ein anderes Template anzeigen. Auch das ist nur sehr schwer möglich. Beispielsweise soll das Modul Loginbox ein Formular anzeigen, wenn der User eingeloggt ist, andernfalls nur den Logout-Link. Wie ich gesagt habe, bestimmt aber das Template den Controller. Nicht umgekehrt. Das macht die Sache sehr kompliziert.
Für mich sind das zu viele Nachteile. Leider habe ich diese Nachteile erst alle bemerkt, nachdem ich meine eigene rudimentäre PHP-Implementierung des HMVC-Patterns angefertigt habe. Dennoch bereue ich die „verschwendete“ Zeit nicht, da ich sehr viel Erfahrung gesammelt habe.
Am meisten stört mich an diesem Konzept, dass die Präsentationsschicht (View) im Mittelpunkt steht und nicht der Controller.
Die Lösung von Kohana
Ein weiteres Framework, das HMVC nativ unterstützt, ist das Kohana-Framework. Bei diesem Framework verschachtelt man Requests. Das alles geschieht im Controller. Auf den ersten Blick hört sich das verrückt an, doch eigentlich ist es das nicht. Erstmal veranschauliche ich euch den Ablauf:
- „Hauptcontroller“ wird geladen
- Diese führt diverse Aktionen aus
- Dann startet er Requests, die genauso wie der Hauptcontroller ausgeführt werden.
- Die Antwort dieser Requests weist er Variablen im View zu
- Im Template werden diese Variablen dann an der richtigen Stelle ausgegeben
Probleme, die mir auf den ersten Blick aufgefallen sind:
- Wie teile ich dem aus dem Controller gestarteten Request mit, dass kein Layout mit geladen werden soll, sondern nur der eigentliche Content?
- Was ist, wenn 2 Module (also in dem Fall Requests) miteinander kommunizieren müssen? Das geht mit Kohana nicht, da die Requests nichts voneinander wissen
Wie man das löst, weiß ich auch nicht. Habe mich nicht weiter damit beschäftigt.
Außerdem hat die Lösung den Nachteil, dass man zur Einbindung von neuen Modulen sowohl Controller als auch View verändern muss. Beim APF müsste man nur die entsprechende Taglib einbetten und der Rest würde automatisch gemacht werden.
Meine Lösung
Ich sehe mich gezwungen, mich zwischen einem der beiden Konzepte zu entscheiden, wenn ich HMVC nutzen will. Und das will ich! Nachdem ich alle Vor- und Nachteile für mich abgewägt habe, tendiere ich eher in Richtung Kohana. Zwar kann ich ein Modul nicht ganz so einfach einbetten wie im APF, aber ich bin flexibler. Ich kann das Template im Controller bestimmen und auch die Ausgabeart (HTML, PDF, XML, JSON) werden im Controller bestimmt. Das alles ist im APF meines Erachtens nicht möglich.
Dennoch stört mich an der Lösung von Kohana die starke Abkapselung der einzelnen Requests. Ein neuer Request ist nämlich wirklich ein neuer Request, so als ob man ihn direkt im Browser gestartet hat. Das heißt, dass sämtliche Parameter des Eltern-Controllers im Kind-Controller unbekannt sind. Und deshalb bin ich derzeit am überlegen, wie ich es anders machen kann. Wer weiß, wann man mal Parameter in mehreren Modulen benötigt.
Sobald es eine Lösung gibt, erscheint sie hier!
[Zitat]Das heißt, dass sämtliche Parameter des Eltern-Controllers im Kind-Controller unbekannt sind.[/Zitat]
Kannst du dafür mal ein Beispiel geben. Ich verstehe nicht ganz was du möchtest. In Kohana ist es sehr wohl möglich über die Bootstrap Datei eine eigene Route anzulegen mit mehren Parametern.
Siehe auch:
http://kerkness.ca/wiki/doku.php?id=routing:routing_basics
http://kohanaframework.org/guide/api/Route
[Zitat]Wie teile ich dem aus dem Controller gestarteten Request mit, dass kein Layout mit geladen werden soll, sondern nur der eigentliche Content?[/Zitat]
Schau dir dazu diese Beispiel an:
http://kerkness.ca/wiki/doku.php?id=template-site:extending_the_template_controller
Dadurch ist es möglich immer das Komplette Layout auszugeben oder auch nur den Content.Damit hast du die Möglichkeit über den Controller die Ausgabe nach deinen wünschen anzupassen oder auch komplett zu überschreiben.
Ich finde die Lösung sehr sehr unschön. Habe es in meinem Framework jetzt anders (besser?) gelöst. Das Framework wird in wenigen Tagen/Wochen realeased und kann dann von jedermann genutzt werden.
Nur mal ein Vorgeschmack von meinem Layout-Controller:
< ?php namespace SHFW\core\Controller; abstract class Controller_Layout extends Controller_Abstract { public function init() { parent::init(); if(!$this->application->getConfig()->exists("layout_path"))
{
die("Bitte layout_path angeben");
}
if(!$this->application->getConfig()->exists("layout"))
{
die("Bitte layout angeben");
}
}
public function beforeRun()
{
parent::beforeRun();
if($this->isRoot() && $this->view instanceof \SHFW\core\View\Templatable)
{
$this->view->setTemplate($this->application->getConfig()->get("layout_path")."/".$this->application->getConfig()->get("layout"));
}
}
}
?>
< ?php namespace SHFW\core\Controller; abstract class Controller_AdvancedLayout extends Controller_Layout { protected $layout = NULL; protected $no_layout = false; public function beforeRun() { parent::beforeRun(); $this->layout = new \stdClass(); //Um Members beschreibbar zu halten
if($this->isRoot() && !$this->no_layout)
{
$this->initLayoutElements();
$this->layout = $this->view;
$this->view = new \SHFW\core\View\HTML($this->application, $this->response, $this->request->getControllerName()."/".$this->request->getActionName().".tpl.php");
}
}
public function afterRun()
{
if($this->isRoot() && !$this->no_layout)
{
$this->layout->content = $this->view;
$this->view = $this->layout;
}
parent::afterRun();
}
public function initLayoutElements() { }
}
?>
Funktioniert problemlos, auch mit AJAX oder XML-Feeds. Wie genau man das nutzt, wird dann in meinem Wiki stehen.
MfG
Simon
Hi Simon,
wollte mal fragen ob dein eigenes Framework mittlerweile fertig ist und ob du es der Öffentlichkeit zugänglich machst?
Viele Grüße
Tobi
Hi,
komplett fertig wird es wohl nie werden.
Wenn du die aktuelle Version möchtest, einfach bei simon@net-developers.de melden!
MfG
Simon
Die aktuellste Version meines HMVC-Frameworks erhaltet ihr ab sofort immer hier: http://www.net-developers.de/blog/2011/02/13/download-info-shfw-hmvc-framework-in-php/
Das ist doch der größte Schwachsinn. Hauptcontroller und mehrere Aktionen, wenn man das schon liest…
Nur weil jemand keine Ahnung hat richtig das MVC-Pattern anzuwenden, wird – auf Teufel komm raus – alles irgendwie verbogen. Ist ein typischer Anfängerfehler zu glauben man muss bei einer Anfrage mehrere Controller und Aktionen durchwandern. Da werden Abhängigkeiten definiert die den ganzen Sinn und Zweck von professioneller Programmierung über Bord werfen. Ist schon ein Unsitte dies in Java zu machen, jetzt versucht man den Blödsinn auch noch in PHP.
Der Vollständigkeit halber die Stellungnahme des Autores vom APF zu den obigen Punkten:
http://www.php.de/software-design/72187-hmvc-apf-vs-kohana.html#post545633
@Roman: Ich freue mich auf Deine Lösung 🙂
@Jochen: Danke für den Link!
Ich habe auch eine andere Kösung wie das APF oder Kohana, ob es eine viel bessere Lösung ist will ich nicht wagen zu behaupten, aber es ist eine.
Ich mache das nicht über Libtags sondern über eine Json Konfiguration, je nach Action wenn es denn nötig ist wird dann das ganze HMVC Objekt zusammengestellt und gecached.
Die Json Configs werden ausgewertet und gemerged.
Das ganze passiert auch über sogenennte Widgets.
„widgettree“ : {
„widget“ : {
„name“ : „mainpage“,
„tpl“ : „mainpage.phtml“,
„widget“ : [
{
„name“ : „header“,
„tpl“ : „header.phtml“,
„controller“ : [
{
„name“ : „Header“,
„actions“ : [
{
„action“ : „track“,
„render“ : false
},
{
„action“ : „count“,
„render“ : true
}
]
},
{
„name“ : „Adition“,
„actions“ : [
{
„action“ : „adcode“,
„render“ : false
},
{
„action“ : „adshow“,
„render“ : true
}
]
}
]
}
]
}
}
Das ist nur ein kleiner Auszug. Jede Action kann ein weiteres Json Config File haben, das wird dann mit dem main File gemerged, der HMVC Baum mit einem Composite zusammengesetzt und das Objekt wird gecached, so wird der Prozess nicht immer durchgerödelt.
Zusäztlich kann man immer noch View Helper oder Action Helper anlegen und diese über einen Mediator ins Spiel bringen.
Mit dem Parameter render bei den Actions kann man bestimmen ob zur Action ein View gerendert werden soll.
Den Wert kann in einer Action aber auch überschreiben um nicht immer die Config anpassen zu müssen.
In den configs gibts noch weitere Einstellung, aber das würde jetzt zu weit führen.
Das nur mal ein Beispiel wie man es auch machen kann.
Gruß Litter
Interessanter Ansatz! Gibt es dein Framework irgendwo zum Anschauen oder ist das rein privat?
Was hältst Du eigentlich von Symfony2? Das „lerne“ ich im Moment.
MfG
Simon
Hallo,
nein ich denke nicht das es das mal öffentlich geben wird, aber genau sagen kann ich es nicht, es muss sich ja auch bewehren.
Aber hier mal ein kleiner Auszug des Mergens der Json Configs.
default.json
{
„widgettree“ : {
„widget“ : {
„name“ : „mainpage“,
„tpl“ : „mainpage.phtml“,
„widget“ : [
{
„name“ : „header“,
„tpl“ : „header.phtml“,
„controller“ : [
{
„name“ : „Header“,
„actions“ : [
{
„action“ : „track“,
„render“ : false
},
{
„action“ : „count“,
„render“ : true
}
]
}
]
},
{
„name“ : „content“,
„tpl“ : „content.phtml“,
„controller“ : [
{
„name“ : „Content“,
„actions“ : [
{
„action“ : „track“,
„render“ : false
},
{
„action“ : „count“,
„render“ : true
}
]
}
],
„widget“ : [
{
„name“ : „sidebar“,
„tpl“ : „sibebar.phtml“,
„controller“ : [
{
„name“ : „Sidebar“,
„actions“ : [
{
„action“ : „track“,
„render“ : false
}
]
}
]
},
{
„name“ : „login“,
„tpl“ : „login.phtml“,
„controller“ : [
{
„name“ : „Login“,
„actions“ : [
{
„action“ : „form“,
„render“ : true
}
]
}
]
},
{
„name“ : „test“,
„tpl“ : „test.phtml“
}
]
}
]
}
}
}
dann die action.json
{
„widgettree“ : {
„widget“ : {
„name“ : „mainpage“,
„tpl“ : „mainpage.phtml“,
„widget“ : [
{
„name“ : „header“
},
{
„name“ : „content“,
„widget“ : [
{
„name“ : „sidebar“,
„remove“ : true
},
{
„name“ : „login“,
„remove“ : true
},
{
„name“ : „test“,
„remove“ : true
}
]
}
]
}
}
}
Und hier nur ein einfacher Auszug der Klasse.
class Config
{
public function merge(array $array1, array $array2)
{
foreach ($array2 as $key => $value) {
if (array_key_exists($key, $array1) && is_array($value)) {
$array1[$key] = $this->merge($array1[$key], $array2[$key]);
if (is_array($value) && array_key_exists(‚remove‘, $value)) {
unset($array1[$key]);
}
if (array_key_exists(‚widget‘, $array1)) {
if (count($array1[$key]) == 0) {
unset($array1[$key]);
}
}
} else {
$array1[$key] = $value;
}
}
return $array1;
}
}
Und das Ende ist dann dieses Array.
Array
(
[widgettree] => Array
(
[widget] => Array
(
[name] => mainpage
[tpl] => mainpage.phtml
[widget] => Array
(
[0] => Array
(
[name] => header
[tpl] => header.phtml
[controller] => Array
(
[0] => Array
(
[name] => Header
[actions] => Array
(
[0] => Array
(
[action] => track
[render] =>
)
[1] => Array
(
[action] => count
[render] => 1
)
)
)
)
)
[1] => Array
(
[name] => content
[tpl] => content.phtml
[controller] => Array
(
[0] => Array
(
[name] => Content
[actions] => Array
(
[0] => Array
(
[action] => track
[render] =>
)
[1] => Array
(
[action] => count
[render] => 1
)
)
)
)
)
)
)
)
)
Ich hoffe du kannst etwas damit anfangen.
Wenn das Array dann verarbeitet wird kommt ein Composite Pattern zum Einsatz was den MVC zusammenbaut. Und jeder Controller mit mehreren Actions instanziert und aufgerufen werden kann an der richtigen Stelle.
Gruß Daniel
Danke!