WordPress mit Zend Framework Beispiel Plugin
Nach einem aktuellen Auftrag, bei dem ich verschiedene WordPress Plugins angepasst habe, habe ich ein Beispiel-Plugin geschrieben, bei dem das ZendFramework integriert ist. Es kann als Grundgerüst für weitere WordPress-Plugins benutzt werden.
Interessantes zum Thema gibt es auch auf folgenden Seiten:
- http://www.leftjoin.net/2011/03/integrating-zend-framework-with-wordpress/
- http://codex.wordpress.org/Writing_a_Plugin
- http://www.devlounge.net/extras/how-to-write-a-wordpress-plugin
Zunächst wird ein Ordner namens gina-sample unter /wp-content/plugins erstellt (es ist egal, aber: GINA steht für Grandgeorg Internet Applications oder gerne auch für GINA is not annoying). In diesem Ordner wird die gina-sample.php Datei erstellt. Diese Datei ist unsere zentrale Plugin-Datei die alle Funktionsaufrufe für die WordPress-Action- und Filter-Hooks enthält. Sie sollte, damit die Übersichtlichkeit gewahrt bleibt, die einzige Datei in diesem Ordner bleiben. Alle anderen Dateien werden in Unterordner gespeichert. Ich lege i.d.R. folgende Unterordner dazu an:
- css
- img
- js
- languages
- php
- lib
- Zend
- views
- scripts
- lib
Zunächst enthält die gina-sample.php-Datei Angaben zum Plugin (die von WordPress ausgewertet werden) und zum Copyright als PHP-Kommentar:
/*
Plugin Name: GINA Sample
Plugin URI: http://intelligibel.de/wordpress/plugin/gina-sample
Description: GINA Sample Plugin is ment for testing plugin development.
Version: 1.0
Author: Viktor Grandgeorg
Author URI: http://intelligibel.de
License: GPL2
Copyright 2011 Viktor Grandgeorg (email : i...@yourdomain.de)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2, as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
Als Nächstes wird eine PHP-Klasse mit statischen Methoden definiert. Es wird eine Klasse verwendet und nicht lediglich Funktionen, um Namenskollisionen von Funktionsnamen zu vermeiden und dennoch kurze und prägnante Benennungen zu ermöglichen. Dabei wird eine Methode namens dispatch() verwendet. Sie soll später alle gewünschten Aufrufe von WordPress “Actions” und “Filters” enthalten:
if (!class_exists("GinaSample")) {
class GinaSample
{
public static function dispatch()
{
// actions
// admin actions
// filters
}
}
GinaSample::dispatch();
}
In der dispatch-Methode wird darüber hinaus auch die ZendFramework-Integration vollzogen, indem ZF in den include-Pfad aufgenommen und eine Instanz von Zend_Loader_Autoloader erzeugt wird. Dabei müssen selbstverständlich die Dateien von ZendFramework unter /wp-content/plugins/gina-sample/lib/Zend liegen. Falls möglich, sollte dort eine symbolische Verknüpfung auf die Dateien erzeugt werden.
Außerdem werden die Get-, Post- und Cookie-Variablen zunächst von überflüssigen Schrägstrichen befreit und anschließend einer lokalen Eigenschaft der Klasse zugeordnet. Dies ist notwendig, da WordPress und/oder andere Plugins während des Verarbeitungsprozesses den Strings der Superglobalen Schrägstriche hinzufügen (ganz gleich, ob magic_quotes aktiviert ist oder nicht). ZF hingegen setzt voraus, dass die Strings ohne zusätzliche Schrägstriche vorhanden sind.
Darüber hinaus werden, falls nötig, die Übersetzungsdateien für das Plugin geladen.
Damit sieht die Klasse wie folgt aus:
define('GINASAMPLE_PLUGIN_PATH', realpath(__DIR__));
class GinaSample
{
public static $adminOptionsName = "GinaSample";
public static $ginaSampleGPC = array();
public static function dispatch()
{
set_include_path(implode(PATH_SEPARATOR, array(get_include_path(),
realpath(GINASAMPLE_PLUGIN_PATH. '/php/lib'))));
require_once 'Zend/Loader/Autoloader.php';
$autoloader = Zend_Loader_Autoloader::getInstance();
// Zend expects magic qoutes to be turned off.
if (get_magic_quotes_gpc()) {
$_POST = array_map('stripslashes_deep', $_POST);
$_GET = array_map('stripslashes_deep', $_GET);
$_COOKIE = array_map('stripslashes_deep', $_COOKIE);
$_REQUEST = array_map('stripslashes_deep', $_REQUEST);
}
// As WP (or Plugins) add slashes to globals anyway, we use our own.
self::$ginaSampleGPC = array(
'_POST' => $_POST,
'_GET' => $_GET,
'_COOKIE' => $_COOKIE
);
if(!load_plugin_textdomain('gina-sample',
'/wp-content/languages/')) {
load_plugin_textdomain('gina-sample', false,
dirname(plugin_basename(__FILE__)) . '/languages');
}
//actions
//admin actions
//filters
}
GinaSample::dispatch();
}
Damit ist das Grundgerüst des Plugins fertig. Anhand von zwei Methoden sollen nun konkrete Funktionen als Beispiel erzeugt werden. Die erste Funktion namens helloWorld() soll die Zeichenkette “Hallo Welt!” in verschiedenen Sprachen am Anfang von jedem Post ausgeben. Die Zweite Funktion namens addContent() soll beliebigen Text zu jedem Post bzw. zu jeder Seite hinzufügen. Im Administrationsbereich von WordPress soll es durch die Methode printAdminPage() möglich sein beide Funktionen ein- und auszuschalten und es soll natürlich möglich sein den Text, der an die Seiten angefügt wird, für die zweite Funktion anzugeben. Für das Formular im Administrationsbereich soll Zend_Form mit Zend_View verwendet werden. Die Einstellungen sollen in der WordPress Datenbank als Konfigurationsoption gespeichert werden. Deshalb wird auch eine Methode namens getAdminOptions() benötigt, die die Werte aus der Datenbank wieder ausliest und Standardwerte setzt, falls keine Werte in der Datenbank vorhanden sind. Das ist unter anderem dann der Fall, wenn das Plugin das erste Mal aktiviert wird. Deshalb soll diese Methode auch bei der Aktivierung ausgeführt werden.
public static function helloWorld()
{
$options = self::getAdminOptions();
if (true == $options['runHelloWord']) {
echo '<h3>' . __('Hello World!', 'gina-sample') . '</h3>';
}
}
public static function addContent($content = '')
{
$options = self::getAdminOptions();
if (true == $options['runAddContent']) {
if (!empty($options['contentAddContent'])) {
$content .= '<p>' . $options['contentAddContent'] . '</p>';
} else {
$content .= '<p>'
. __('You hit GinaSample::addContent();',
'gina-sample')
. '</p>';
}
}
return $content;
}
public static function getAdminOptions()
{
$defaultOptions = array(
'runHelloWord' => true,
'runAddContent' => true,
'contentAddContent' => ''
);
$options = get_option(self::$adminOptionsName, false);
if (false === $options) {
$options = $defaultOptions;
update_option(self::$adminOptionsName, $options);
}
return $options;
}
public static function printAdminPage()
{
$_POST = self::$ginaSampleGPC{'_POST'};
$_GET = self::$ginaSampleGPC{'_GET'};
$_COOKIE = self::$ginaSampleGPC{'_COOKIE'};
$options = self::getAdminOptions();
$view = new Zend_View();
$view->setBasePath(realpath(GINASAMPLE_PLUGIN_PATH . '/php/views/'));
$form = new Zend_Form();
$form->setAction($_SERVER['REQUEST_URI'])
->setMethod('post')
->setAttrib('id', 'ginaSampleAdminForm')
->addElement('radio', 'ginaSampleOptionRunHelloWord', array(
'label' => __('Show Hello World in Posts?', 'gina-sample'),
'multioptions' => array('true' => __('Yes', 'gina-sample'),
'false' => __('No', 'gina-sample')),
'separator' => ' '))
->addElement('radio', 'ginaSampleOptionRunAddContent', array(
'label' => __('Allow Content Added to the End of a Post?',
'gina-sample'),
'multioptions' => array('true' => __('Yes', 'gina-sample'),
'false' => __('No', 'gina-sample')),
'separator' => ' '))
->addElement('textarea', 'ginaSampleContentAddContent', array(
'label' => __('Content to Add to the End of a Post',
'gina-sample'),
'cols' => 35,
'rows' => 5))
->addElement('submit', 'updateGinaSamplePluginSettings', array(
'label' => __('Update Settings', 'gina-sample')))
;
if ($form->isValid($_POST)
&& isset($_POST['updateGinaSamplePluginSettings'])) {
$formValues = $form->getValues();
if (isset($formValues['ginaSampleOptionRunHelloWord'])) {
if ('true' == $formValues['ginaSampleOptionRunHelloWord']) {
$options['runHelloWord'] = true;
} else {
$options['runHelloWord'] = false;
}
}
if (isset($formValues['ginaSampleOptionRunAddContent'])) {
if ('true' == $formValues['ginaSampleOptionRunAddContent']) {
$options['runAddContent'] = true;
} else {
$options['runAddContent'] = false;
}
}
if (isset($_POST['ginaSampleContentAddContent'])) {
$options['contentAddContent'] = apply_filters(
'content_save_pre',
$formValues['ginaSampleContentAddContent']);
}
update_option(self::$adminOptionsName, $options);
$view->update = true;
}
$form->populate(array(
'ginaSampleOptionRunHelloWord' =>
(true === $options['runHelloWord'])? 'true' : 'false',
'ginaSampleOptionRunAddContent' =>
(true === $options['runAddContent'])? 'true' : 'false',
'ginaSampleContentAddContent' => $options['contentAddContent']
));
$view->form = $form->render($view);
echo $view->render('admin.phtml');
}
Für das Zend-View-Objekt in der Methode printAdminPage() muss noch die Datei admin.phtml im Unterordner views\scripts\ (s.o.) mit folgendem Inhalt definiert werden:
<div class="wrap">
<h2>Gina Sample Plugin</h2>
<?php if (true === $this->update): ?>
<div class="updated"><p><strong><?php _e('Settings Updated.', 'gina-sample'); ?></strong></p></div>
<?php endif; ?>
<?php echo $this->form ?>
</div>
Schließlich wird noch eine Methode in der GinaSample Klasse definiert, die die Administrationsseite zum AdminPanel hinzufügt so, dass sie im Menü Einstellungen zu sehen ist:
public function addAdminPanel()
{
if (function_exists('add_options_page')) {
add_options_page('Gina Sample Plugin', 'Gina Sample Plugin',
'manage_options', __FILE__,
array('GinaSample', 'printAdminPage'));
}
}
Schlußendlich müssen dann in der dispatch-Methode nur noch die jeweiligen Aktionen und Filter aufgerufen werden:
//actions
add_action('the_post', array('GinaSample', 'helloWorld'));
//admin actions
add_action('admin_menu', array('GinaSample', 'addAdminPanel'), 0);
add_action('activate_gina-sample/gina-sample.php',
array('GinaSample', 'getAdminOptions'));
//filters
add_filter('the_content', array('GinaSample', 'addContent'));
Um zu entscheiden, an welcher Stelle das Plugin eingeklinkt werden soll, ist die WP-Dokumentation zu Aktionen und zu Filtern zu konsultieren.
Das ganze Beispiel-Plugin kann hier heruntergeladen werden.
Die ZIP-Datei ist inklusive deutscher Sprachdateien, enthält aber nicht das ZendFramework.
Hallo Viktor
Besten Dank für dein Tutorial. Obwohl ich die Zend-Library bereits bei meinen WordPress Plugins integriert habe, hatte ich heute ein Problem diese bei einem anderen Webhoster zum laufen zu bekommen.
Dank deiner Anleitung war der Fehler auch schnell gefunden: Die Aufnahme der Library in den PHP-Includepfad fehlte. Nun läuft alles.
Was ich hingegen anders mache als du, ich definiert die WordPress action&filter innerhalb des Konstruktors der jeweiligen Klassen und nicht in einer eigenen dispatch() Methode. Gibts dafür einen speziellen Grund das du diese zentral kapselst?
Besten Dank & Gruss
Roman
@Roman
Da ich mit statischen Methoden arbeite, wird die Klasse nicht instanziiert und hat keinen Konstruktor. Ich verwende eine Klasse hier überhaupt mehr oder weniger nur um die Funktionen zu strukturieren und Namenskollisionen zu vermeiden – siehe auch http://codex.wordpress.org/Writing_a_Plugin:
“All the functions in your Plugin need to have unique names that are different from functions in the WordPress core, other Plugins, and themes. For that reason, it is a good idea to use a unique function name prefix on all of your Plugin’s functions. A far superior possibility is to define your Plugin functions inside a class (which also needs to have a unique name).”
So vermeide ich aber auch eine Variable im global scope setzen zu müssen, was ich bei einer Instanz ja müsste. Da bestünde die Gefahr, dass der Variablenname schon anderweitig verwendet wird.
Eine Instanz würde nur Sinn machen, wenn man diese Instanz an anderer Stelle weiterverwenden wollte. Das ist hier aber nicht der Fall – könnte es aber im Prinzip durchaus sein.
Grundsätzlich würde ich aber überlegen, ob es Sinn macht die actions und filter gerade im Konstruktor zu verorten. Das ist i.d.R. ungünstig, da man keine Instanz erzeugen kann ohne, dass diese ausgeführt werden. Außerdem hat der Konstruktor kein Rückgabewert und so richtig leserlich ist es auch nicht wenn eine Klasse nur Intantiiert wird und sonst keine Methodenaufrufe mehr erfolgen. Ich würde deshalb immer etwas schreiben, wie:
$plugin = new GinaSample();
$plugin->run(); // oder dispatch();
Hallo Viktor
Danke für deine Ausführungen.
Um keine Variable im global scope setzen zu müssen, speichere ich diese via Zend_Registry ab. So habe ich überall Zugriff.
Betreffend den action/filters hast du Recht. Macht durchaus mehr Sinn diese erst mittels expiziten call auszuführen.
Danke & Gruss
Roman