Hier, vous avez appris à internationaliser et régionaliser vos applications symfony. Encore une fois, grâce à la norme ICU et beaucoup de helpers, symfony le fait vraiment facilement.
Aujourd'hui, nous allons parler des plugins : ce qu'ils sont, ce que vous pouvez empaqueter dans un plugin et comment ils peuvent être utilisés.
Plugins
Un plugin symfony
Un plugin symfony offre une façon d'empaqueter et distribuer une partie de vos fichiers du projet. Comme un projet, un plugin peut contenir des classes, des helpers, de la configuration, des tâches, des modules, des schémas et même des ressources web.
Plugins privés
La première utilisation des plugins est de faciliter le partage de code entre vos applications, ou même entre différents projets. Rappelons que les applications symfony ne partagent que le modèle. Les plugins fournissent un moyen de partager plus de composants entre les applications.
Si vous avez besoin de réutiliser le même schéma pour des projets différents,
ou les mêmes modules, déplacez les dans un plugin. Comme un plugin est simplement un répertoire, vous
pouvez le déplacer très facilement en créant un dépôt SVN et en utilisant svn:externals
, ou en
copiant simplement les fichiers d'un projet à l'autre.
Nous les appelons "plugins privés" parce que leur utilisation est restreinte à un unique développeur ou entreprise. Ils ne sont pas accessibles au public.
tip
Vous pouvez même créer un package de vos plugins privé, créer votre propre canal
de plugin symfony et les installer via la tâche plugin:install
.
Plugins publiques
Les plugins publiques sont disponibles pour la communauté pour être télécharger et installer. Au cours de
ce tutoriel, nous avons utilisé un couple de plugins publiques : sfGuardPlugin
et
sfFormExtraPlugin
.
Ce sont exactement les mêmes que pour les plugins privés. La seule différence est que n'importe qui peut les installer dans leurs projets. Vous apprendrez plus tard la manière de publier et d'héberger un plugin publique sur le site de symfony.
Une façon différente d'organiser le code
Il y a une autre façon de voir les plugins et comment les utiliser. Oubliez la
réutilisation et le partage. Les plugins peuvent être utilisées d'une façon différente
pour organiser votre code. Au lieu d'organiser les fichiers par couche : tous les modèles
dans le répertoire lib/model/
, les templates dans le répertoire templates/
, ...; les
fichiers sont mis en place par fonction : tous les fichiers emplois (le modèle, les modules
et les templates), tous les fichiers CMS, et ainsi de suite.
Structure des fichiers du plugin
Un plugin est juste une structure de répertoire avec des fichiers organisés selon
une structure prédéfinie, selon la nature des fichiers. Aujourd'hui, nous allons passer la plupart
du code que nous avons écrit pour Jobeet dans un sfJobeetPlugin
. La structure de base que nous
allons utiliser est la suivante :
sfJobeetPlugin/ config/ sfJobeetPluginConfiguration.class.php // Plugin initialization schema.yml // Database schema routing.yml // Routing lib/ Jobeet.class.php // Classes helper/ // Helpers filter/ // Filter classes form/ // Form classes model/ // Model classes task/ // Tasks modules/ job/ // Modules actions/ config/ templates/ web/ // Assets like JS, CSS, and images
Le plugin Jobeet
L'initialisation d'un plugin est aussi simple que de créer un nouveau répertoire
sous plugins/
. Pour Jobeet, nous allons créer un répertoire sfJobeetPlugin
:
$ mkdir plugins/sfJobeetPlugin
Ensuite, activez le plugin sfJobeetPlugin
dans le fichier config/ProjectConfiguration.class.php
.
public function setup() { $this->enablePlugins(array( 'sfPropelPlugin', 'sfPropelGuardPlugin', 'sfFormExtraPlugin', 'sfJobeetPlugin' )); }
note
Tous les plugins doivent se terminer par un Plugin
. C'est aussi une bonne habitude de les
préfixer avec sf
, même si elle n'est pas obligatoire.
Le Modèle
Premièrement, déplacez le fichier config/schema.yml
vers plugins/sfJobeetPlugin/config/
:
$ mkdir plugins/sfJobeetPlugin/config/ $ mv config/schema.yml plugins/sfJobeetPlugin/config/schema.yml
note
Toutes les commandes sont pour Unix et assimilés. Si vous utilisez Windows, vous
pouvez glisser et déposer des fichiers dans l'explorateur. Et si vous utilisez Subversion,
ou tout autre outil pour gérer votre code, utilisez les outils intégrés qu'ils fournissent
(comme svn mv
pour déplacer des fichiers).
Déplacez les fichiers du modèle, des formulaires et des filtres vers plugins/sfJobeetPlugin/lib/
:
$ mkdir plugins/sfJobeetPlugin/lib/ $ mv lib/model/ plugins/sfJobeetPlugin/lib/ $ mv lib/form/ plugins/sfJobeetPlugin/lib/ $ mv lib/filter/ plugins/sfJobeetPlugin/lib/ $ rm -rf plugins/sfJobeetPlugin/lib/model/sfPropelGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/form/sfPropelGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/filter/sfPropelGuardPlugin
Supprimez le fichier plugins/sfJobeetPlugin/lib/form/BaseForm.class.php
.
$ rm plugins/sfJobeetPlugin/lib/form/BaseForm.class.php
Si vous deviez exécuter la tâche propel:build --model
maintenant, symfony générerait
toujours les fichiers dans lib/model/
, ce qui n'est pas ce que nous voulons. Le répertoire
de sortie de Propel peut être configuré en ajoutant une option package
. Ouvrez le
schema.yml
et ajoutez la configuration suivante :
# plugins/sfJobeetPlugin/config/schema.yml propel: _attributes: { package: plugins.sfJobeetPlugin.lib.model }
Maintenant symfony générera ces fichiers sous le répertoire
plugins/sfJobeetPlugin/lib/model/
. Les formulaires et les filtres contruits
prennent aussi en compte cette configuration où ils génèrent des fichiers.
La tâche propel:build --sql
génère un fichier pour créer les tables. Comme le
fichier est nommé après le package, supprimez l'actuel :
$ rm data/sql/lib.model.schema.sql
Maintenant si vous exécutez propel:build --all --and-load
, symfony générera les fichiers
sous le répertoire du plugin lib/model/
comme prévu :
$ php symfony propel:build --all --and-load --no-confirmation
Après l'exécution de la tâche, vérifiez qu'aucun répertoire lib/model/
n'a été créé.
La tâche a créé cependant les répertoires lib/form/
et lib/filter/
. Ils comportent
tous les deux des classes de base pour tous les formulaires de Propel dans votre projet.
Comme ces fichiers sont globaux pour un projet, retirez-les du plugin :
$ rm plugins/sfJobeetPlugin/lib/form/BaseFormPropel.class.php $ rm plugins/sfJobeetPlugin/lib/filter/BaseFormFilterPropel.class.php
Vous pouvez également déplacer le fichier Jobeet.class.php
vers le plugin :
$ mv lib/Jobeet.class.php plugins/sfJobeetPlugin/lib/
Comme nous avons déplacé des fichiers, videz le cache :
$ php symfony cc
tip
Si vous utilisez un accélérateur PHP comme APC, les choses deviennent étranges à ce stade, redémarrez Apache.
Maintenant que tous les fichiers du modèle ont été déplacés dans le plugin, exécuter les tests pour vérifier que tout fonctionne toujours très bien :
$ php symfony test:all
Les contrôleurs et les vues
L'étape logique suivante consiste à déplacer les modules vers le plugin. Pour éviter les collisions de nom de module, c'est une bonne habitude de faire précéder les noms des modules du plugin par le nom du plugin :
$ mkdir plugins/sfJobeetPlugin/modules/ $ mv apps/frontend/modules/affiliate plugins/sfJobeetPlugin/modules/sfJobeetAffiliate $ mv apps/frontend/modules/api plugins/sfJobeetPlugin/modules/sfJobeetApi $ mv apps/frontend/modules/category plugins/sfJobeetPlugin/modules/sfJobeetCategory $ mv apps/frontend/modules/job plugins/sfJobeetPlugin/modules/sfJobeetJob $ mv apps/frontend/modules/language plugins/sfJobeetPlugin/modules/sfJobeetLanguage
Pour chaque module, vous devez également modifier le nom des classes dans toutes les
fichiers actions.class.php
et components.class.php
(par exemple, la classe
affiliateActions
doit être renommé en sfJobeetAffiliateActions
).
Les appels include_partial()
et include_component()
doivent aussi être changés
dans les Templates suivants :
sfJobeetAffiliate/templates/_form.php
(changezaffiliate
ensfJobeetAffiliate
)sfJobeetCategory/templates/showSuccess.atom.php
sfJobeetCategory/templates/showSuccess.php
sfJobeetJob/templates/indexSuccess.atom.php
sfJobeetJob/templates/indexSuccess.php
sfJobeetJob/templates/searchSuccess.php
sfJobeetJob/templates/showSuccess.php
apps/frontend/templates/layout.php
Mettez à jour les actions search
et delete
:
// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php class sfJobeetJobActions extends sfActions { public function executeSearch(sfWebRequest $request) { $this->forwardUnless($query = $request->getParameter('query'), 'sfJobeetJob', 'index'); $this->jobs = JobeetJobPeer::getForLuceneQuery($query); if ($request->isXmlHttpRequest()) { if ('*' == $query || !$this->jobs) { return $this->renderText('No results.'); } return $this->renderPartial('sfJobeetJob/list', array('jobs' => $this->jobs)); } } public function executeDelete(sfWebRequest $request) { $request->checkCSRFProtection(); $jobeet_job = $this->getRoute()->getObject(); $jobeet_job->delete(); $this->redirect('sfJobeetJob/index'); } // ... }
Maintenant, modifiez le fichier routing.yml
pour prendre ces changements en compte :
# apps/frontend/config/routing.yml affiliate: class: sfPropelRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: GET } prefix_path: /:sf_culture/affiliate module: sfJobeetAffiliate requirements: sf_culture: (?:fr|en) api_jobs: url: /api/:token/jobs.:sf_format class: sfPropelRoute param: { module: sfJobeetApi, action: list } options: { model: JobeetJob, type: list, method: getForToken } requirements: sf_format: (?:xml|json|yaml) category: url: /:sf_culture/category/:slug.:sf_format class: sfPropelRoute param: { module: sfJobeetCategory, action: show, sf_format: html } options: { model: JobeetCategory, type: object, method: doSelectForSlug } requirements: sf_format: (?:html|atom) sf_culture: (?:fr|en) job_search: url: /:sf_culture/search param: { module: sfJobeetJob, action: search } requirements: sf_culture: (?:fr|en) job: class: sfPropelRouteCollection options: model: JobeetJob column: token object_actions: { publish: PUT, extend: PUT } prefix_path: /:sf_culture/job module: sfJobeetJob requirements: token: \w+ sf_culture: (?:fr|en) job_show_user: url: /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: model: JobeetJob type: object method_for_criteria: doSelectActive param: { module: sfJobeetJob, action: show } requirements: id: \d+ sf_method: GET sf_culture: (?:fr|en) change_language: url: /change_language param: { module: sfJobeetLanguage, action: changeLanguage } localized_homepage: url: /:sf_culture/ param: { module: sfJobeetJob, action: index } requirements: sf_culture: (?:fr|en) homepage: url: / param: { module: sfJobeetJob, action: index }
Si vous essayez de parcourir le site Jobeet maintenant, vous allez avoir des exceptions
vous indiquant que les modules ne sont pas activés. Comme les plugins sont partagés par
toutes les applications dans un projet, vous devez activer spécifiquement le module dont
vous avez besoin pour une application donnée dans le fichier de configuration settings.yml
:
# apps/frontend/config/settings.yml all: .settings: enabled_modules: - default - sfJobeetAffiliate - sfJobeetApi - sfJobeetCategory - sfJobeetJob - sfJobeetLanguage
La dernière étape de la migration est de fixer les tests fonctionnels où nous les testons pour le nom du module.
Les tâches
Les tâches peuvent être déplacés vers le plugin assez facilement :
$ mv lib/task plugins/sfJobeetPlugin/lib/
Les fichiers i18n
Un plugin peut aussi contenir des fichiers XLIFF :
$ mv apps/frontend/i18n plugins/sfJobeetPlugin/
Le Routage
Un plugin peut également contenir des règles de routage :
$ mv apps/frontend/config/routing.yml plugins/sfJobeetPlugin/config/
Les ressources
Même si c'est un peu contre-intuitif, un plugin peut également contenir des ressources
web comme les images, les feuilles de style et les JavaScripts. Comme nous ne voulons pas
distribuer le plugin Jobeet, cela n'a pas vraiment de sens, mais cela est possible en créant
un répertoire plugins/sfJobeetPlugin/web/
.
Les ressources d'un plugin doit être accessible dans un répertoire web/
pour
être lisible à partir d'un navigateur. Le plugin:publish-assets
adresse cela en
créant des liens symboliques sous le système Unix et en copiant les fichiers sur la
plate-forme Windows :
$ php symfony plugin:publish-assets
L'utilisateur
Le déplacement des méthodes de la classe myUser
qui traitent de l'historique
des emplois est un peu plus compliqué. Nous pourrions créer une classe JobeetUser
et faire hérité myUser
. Mais il y a un meilleur moyen, surtout si plusieurs plugins
souhaitent ajouter de nouvelles méthodes pour la classe.
Les objets du noyau de symfony notifient les événements au cours de leur cycle de vie que
vous pouvez écouter. Dans notre cas, nous devons écouter l'événement user.method_not_found
,
qui se produit lorsqu'une méthode non définie est appelée sur l'objet sfUser
.
Quand symfony est initialisé, tous les plugins sont également initialisés s'ils ont une classe de configuration de plugin :
// plugins/sfJobeetPlugin/config/sfJobeetPluginConfiguration.class.php class sfJobeetPluginConfiguration extends sfPluginConfiguration { public function initialize() { $this->dispatcher->connect('user.method_not_found', array('JobeetUser', 'methodNotFound')); } }
Les notifications d'événements sont gérés par
sfEventDispatcher
,
l'objet du dispatcher d'événement. L'enregistrement d'un listener est aussi simple
que d'appeler la méthode connect()
. La méthode connect()
connecte un nom
d'événement à un PHP appelable.
note
Un PHP appelable est une
variable PHP qui peut être utilisée par la fonction call_user_func()
et renvoie
true
lorsque elle est passée à la fonction is_callable()
. Une chaîne représente
une fonction et un tableau peut représenter une méthode d'objet ou une méthode de classe.
Avec le code en place ci-dessus, l'objet myUser
appelera la méthode statique
methodNotFound()
de la classe JobeetUser
chaque fois qu'il est incapable de
trouver une méthode. Il appartient ensuite à la méthode methodNotFound()
de
traiter la méthode manquante ou non.
Supprimez toutes les méthodes de la classe myUser
et créez la classe JobeetUser
:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { } // plugins/sfJobeetPlugin/lib/JobeetUser.class.php class JobeetUser { static public function methodNotFound(sfEvent $event) { if (method_exists('JobeetUser', $event['method'])) { $event->setReturnValue(call_user_func_array( array('JobeetUser', $event['method']), array_merge(array($event->getSubject()), $event['arguments']) )); return true; } } static public function isFirstRequest(sfUser $user, $boolean = null) { if (is_null($boolean)) { return $user->getAttribute('first_request', true); } else { $user->setAttribute('first_request', $boolean); } } static public function addJobToHistory(sfUser $user, JobeetJob $job) { $ids = $user->getAttribute('job_history', array()); if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId()); $user->setAttribute('job_history', array_slice($ids, 0, 3)); } } static public function getJobHistory(sfUser $user) { return JobeetJobPeer::retrieveByPks($user->getAttribute('job_history', array())); } static public function resetJobHistory(sfUser $user) { $user->getAttributeHolder()->remove('job_history'); } }
Lorsque le dispatcher appelle la méthode methodNotFound()
, il passe un
objet sfEvent
.
Si la méthode existe dans la classe JobeetUser
, il est appelé et sa valeur
retournée est retournée par la suite au notificateur. Sinon, symfony va essayer le
listener suivant enregistré ou lever une exception.
La méthode getSubject()
retourne le notificateur de l'événement, qui dans ce
cas est l'objet actuel myUser
.
La structure par défaut contre l'architecture du plugin
L'utilisation de l'architecture du plugin vous permet d'organiser votre code d'une manière différente :
L'utilisation du plugin
Lorsque vous commencez à mettre en œuvre une nouvelle fonctionnalité, ou si vous essayez de résoudre un problème web classique, y a fort à parier que quelqu'un a déjà résolu le même problème et peut-être emballé la solution dans un plugin symfony. Pour rechercher un plugin publique de symfony, accédez à la section des plugins du site de symfony.
Comme un plugin est contenu dans un répertoire, il y a plusieurs façons de l'installer :
- Utiliser la tâche
plugin:install
(cela ne fonctionne que si le développeur du plugin a créé un package du plugin et l'a envoyé sur le site symfony) - Le téléchargement du package et la décompression manuelle sous le répertoire
plugins/
(il faut aussi que le développeur ait envoyé un package) - La création d'un
svn:externals
dansplugins/
pour le plugin (il ne fonctionne que si l'hôte du développeur du plugin est un plugin sur Subversion)
Les deux dernières méthodes sont faciles, mais manquent de souplesse. La première manière vous permet d'installer la dernière version selon la version du projet symfony, il est facile de mettre à jour la dernière version stable, et de gérer facilement les dépendances entre les plugins.
La contribution d'un Plugin
Faire le package d'un plugin
Pour créer un package de plugin, vous devez ajouter quelques fichiers obligatoires
à la structure du répertoire du plugin. D'abord, créez un fichier README
à la racine
du répertoire de plugin, et expliquez comment installer le plugin, ce qu'il fait et ce qu'il
fait pas. Le fichier README
doit être formaté avec le
format Markdown. Ce fichier
sera utilisé sur le site symfony comme la pièce principale de la documentation.
Vous pouvez tester la conversion de votre fichier README au format HTML en utilisant le
plugin dingus de symfony.
Vous devez également créer un fichier LICENSE
. Le choix d'une licence n'est pas
une tâche facile, mais la section plugin de symfony répertorie uniquement les plugins
qui sont publiées sous une licence similaire à celle symfony (MIT, BSD, LGPL, et PHP). Le
contenu du fichier LICENSE
sera affiché sous l'onglet licence de la page publique
de votre plugin.
La dernière étape est de créer un fichier package.xml
à la racine du répertoire
du plugin. Ce fichier package.xml
suit la
syntaxe d'un paquet PEAR.
note
La meilleure façon d'apprendre la syntaxe du package.xml
est certainement de
copier celui qui est utilisé par un
plugin existant.
Le fichier package.xml
est composé de plusieurs parties comme vous pouvez le
voir dans ce modèle par exemple :
<!-- plugins/sfJobeetPlugin/package.xml --> <?xml version="1.0" encoding="UTF-8"?> <package packagerversion="1.4.1" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd" > <name>sfJobeetPlugin</name> <channel>plugins.symfony-project.org</channel> <summary>A job board plugin.</summary> <description>A job board plugin.</description> <lead> <name>Fabien POTENCIER</name> <user>fabpot</user> <email>fabien.potencier@symfony-project.com</email> <active>yes</active> </lead> <date>2008-12-20</date> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <notes /> <contents> <!-- CONTENT --> </contents> <dependencies> <!-- DEPENDENCIES --> </dependencies> <phprelease> </phprelease> <changelog> <!-- CHANGELOG --> </changelog> </package>
La balise <contents>
contient les fichiers qui doivent être placés dans le paquet :
<contents> <dir name="/"> <file role="data" name="README" /> <file role="data" name="LICENSE" /> <dir name="config"> <file role="data" name="config.php" /> <file role="data" name="schema.yml" /> </dir> <!-- ... --> </dir> </contents>
La balise <dependencies>
référence toutes les dépendances du plugin qu'il pourrait
y avoir : PHP, symfony et aussi d'autres plugins. Cette information est utilisée par la
tâche plugin:install
pour installer la meilleure version du plugin pour l'environnement
du projet et d'installer aussi les dépendances requises du plugin le cas échéant.
<dependencies> <required> <php> <min>5.0.0</min> </php> <pearinstaller> <min>1.4.1</min> </pearinstaller> <package> <name>symfony</name> <channel>pear.symfony-project.com</channel> <min>1.3.0</min> <max>1.5.0</max> <exclude>1.5.0</exclude> </package> </required> </dependencies>
Vous devez toujours déclarer une dépendance sur symfony, comme nous l'avons fait ici.
La déclaration d'une version minimum et maximum permet à plugin:install
de savoir
quelle version de symfony est obligatoire car les versions de symfony peuvent avoir de
légères différences dans l'API.
La déclaration d'une dépendance avec un autre plugin est aussi possible :
<package> <name>sfFooPlugin</name> <channel>plugins.symfony-project.org</channel> <min>1.0.0</min> <max>1.2.0</max> <exclude>1.2.0</exclude> </package>
La balise <changelog>
est facultative, mais donne des informations utiles sur ce
qui a changé entre les versions. Cette information est disponible sous l'onglet
"Changelog" et aussi dans le
flux du plugin.
<changelog> <release> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <date>2008-12-20</date> <license>MIT</license> <notes> * fabien: First release of the plugin </notes> </release> </changelog>
Hébergement d'un plugin sur le site web de symfony
Si vous développez un plugin utile et vous souhaitez le partager avec la communauté de symfony, créez un compte symfony si vous n'en avez pas déjà un, puis créer un nouveau plugin.
Vous deviendrez automatiquement l'administrateur pour le plugin et vous verrez un onglet "admin" dans l'interface. Dans cet onglet, vous trouverez tout ce dont vous avez besoin pour gérer votre plugin et téléchargez vos paquets.
note
Le FAQ du plugin contient beaucoup d'informations utiles pour les développeurs de plugin.
À demain
Créer des plugins et les partager avec la communauté est l'une des meilleures façons de contribuer en retour au projet symfony. Cela est si facile, que le dépôt de plugin de symfony est rempli de plugins utiles, fun, mais aussi de plugins ridicules.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.