Inicializar un plugin es tan sencillo como crear un nuevo directorio bajo el directorio plugins/
. Para el plugin de Jobeet, crea un directorio llamado sfJobeetPlugin
:
$ mkdir plugins/sfJobeetPlugin
Nota El nombre de todos los plugins debe acabar con la palabra Plugin
. También es recomendable utilizar el prefijo sf
, aunque no es obligatorio.
20.3.1. El modelo
En primer lugar, mueve el archivo config/schema.yml
a plugins/sfJobeetPlugin/config/
:
$ mkdir plugins/sfJobeetPlugin/config/ $ mv config/schema.yml plugins/sfJobeetPlugin/config/schema.yml
Nota Todos los comandos que mostramos en este tutorial son los apropiados para los entornos tipo Unix. Si utilizas Windows, puedes copiar y pegar los archivos utilizando el explorador de archivos. Si utilizas Subversion o cualquier otra herramienta para gestionar tu código, utiliza las herramientas que incluyen para mover código (como por ejemplo svn mv
para mover los archivos).
A continuación, mueve todos los archivos del modelo, formularios y filtros al directorio 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/
Si ahora ejecutas la tarea propel:build-model
, Symfony sigue generando todos sus archivos en el directorio lib/model/
, que es justo lo que no queremos. El directorio en el que Propel genera sus archivos se puede configurar mediante la opción package
. Abre el archivo schema.yml
y añade la siguiente configuración:
# plugins/sfJobeetPlugin/config/schema.yml
propel:
_attributes: { package: plugins.sfJobeetPlugin.lib.model }
Ahora Symfony genera sus archivos en el directorio plugins/sfJobeetPlugin/lib/model/
. Los generadores de formularios y de filtros también tienen en consideración esta configuración cuando generan sus archivos.
La tarea propel:build-sql
genera un archivo SQL para crear las tablas de la base de datos. Como el archivo se llama igual que el paquete, elimina el archivo actual:
$ rm data/sql/lib.model.schema.sql
Si ejecutas ahora la tarea propel:build-all-load
, Symfony genera todos sus archivos en el directorio lib/model/
del plugin:
$ php symfony propel:build-all-load --no-confirmation
Después de ejecutar la tarea anterior, asegúrate de que no se ha creado un directorio llamado lib/model/
. Sin embargo, la tarea anterior si que ha creado los directorios lib/form/
y lib/filter/
. Estos directorios incluyen las clases base de todos los formularios Propel del proyecto.
Como estos archivos son globales para un proyecto, puedes eliminarlos en el plugin:
$ rm plugins/sfJobeetPlugin/lib/form/BaseFormPropel.class.php $ rm plugins/sfJobeetPlugin/lib/filter/BaseFormFilterPropel.class.php
Nota Si utilizas Symfony 1.2.0 o 1.2.1, el archivo del formulario base de los filtros se encuentra en el directorio plugins/sfJobeetPlugin/lib/filter/base/
.
También puedes mover el archivo Jobeet.class.php
al plugin:
$ mv lib/Jobeet.class.php plugins/sfJobeetPlugin/lib/
Como hemos movido muchos archivos y clases, no te olvides de borrar la cache de Symfony:
$ php symfony cc
Nota Si utilizas un acelerador de PHP tipo APC, es posible que se produzcan algunos errores en este punto, por lo que te recomendamos que reinicies Apache.
Después de mover todos los archivos del modelo al plugin, ejecuta las pruebas automáticas para comprobar que todo sigue funcionando correctamente:
$ php symfony test:all
20.3.2. Los controladores y las vistas
El siguiente paso lógico consiste en mover los módulos al directorio del plugin. Para evitar duplicidades con el nombre de los módulos, te aconsejamos prefijar el nombre de cada módulo con el nombre del propio 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
No te olvides de modificar también el nombre de la clase en todos los archivos actions.class.php
y components.class.php
de cada módulo (por ejemplo, la clase affiliateActions
se debe renombrar a sfJobeetAffiliateActions
).
Cambia también las llamadas a include_partial()
y include_component()
en las siguientes plantillas:
sfJobeetAffiliate/templates/_form.php
(cambiaaffiliate
porsfJobeetAffiliate
)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
Actualiza las acciones search
y delete
:
// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php
class sfJobeetJobActions extends sfActions
{
public function executeSearch(sfWebRequest $request)
{
if (!$query = $request->getParameter('query'))
{
return $this->forward('sfJobeetJob', 'index');
}
$this->jobs = JobeetJobPeer::getForLuceneQuery($query);
if ($request->isXmlHttpRequest())
{
if ('*' == $query || !$this->jobs)
{
return $this->renderText('No results.');
}
else
{
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');
}
// ...
}
Por último, modifica el archivo routing.yml
para que tenga en cuenta todos los cambios anteriores:
# 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 ahora accedes al sitio web de Jobeet, verás que se muestran excepciones indicando que los módulos no están activados. Como los plugins están disponibles en todas las aplicaciones de un mismo proyecto, debes indicar explícitamente en el archivo de configuración settings.yml
los módulos que están activados en cada aplicación:
# apps/frontend/config/settings.yml
all:
.settings:
enabled_modules:
- default
- sfJobeetAffiliate
- sfJobeetApi
- sfJobeetCategory
- sfJobeetJob
- sfJobeetLanguage
El último paso de la migración consiste en arreglar las pruebas funcionales en las que probamos el nombre del módulo.
20.3.3. Las tareas
Mover las tareas al plugin es muy sencillo:
$ mv lib/task plugins/sfJobeetPlugin/lib/
20.3.4. Los archivos de internacionalización
Los plugins también pueden contener archivos en formato XLIFF:
$ mv apps/frontend/i18n plugins/sfJobeetPlugin/
20.3.5. El sistema de enrutamiento
Los plugins también pueden incluir sus propias reglas en el sistema de enrutamiento:
$ mv apps/frontend/config/routing.yml plugins/sfJobeetPlugin/config/
20.3.6. Los archivos CSS y JavaScript
A pesar de que puede no parecer evidente, los plugins también pueden contener archivos web como imágenes, hojas de estilos y archivos JavaScript. Como no vamos a redistribuir Jobeet como plugin, no tiene sentido que añadamos todos estos archivos, pero si quieres hacerlo, crea un directorio llamado plugins/sfJobeetPlugin/web/
y copia en el todos estos archivos.
Para que los archivos web del plugin se puedan ver desde el navegador, es necesario hacerlos accesibles en el directorio web/
del proyecto. La tarea plugin:publish-assets
se encarga de ello creando enlaces simbólicos en sistemas operativos Unix y copiando los archivos en sistemas operativos Windows:
$ php symfony plugin:publish-assets
20.3.7. El usuario
Mover los métodos de la clase myUser
que se encargan de crear el historial de las ofertas de trabajo visitadas es un poco más complicado. Se podría crear una clase llamada JobeetUser
y hacer que myUser
herede de ella. No obstante, existe una forma mejor de hacerlo, sobre todo si varios plugins diferentes quieren añadir métodos a la clase.
Los objetos internos de Symfony notifican durante su tiempo de vida diferentes eventos que podemos escuchar. En nuestro caso, queremos escuchar el evento user.method_not_found
, que se notifica cuando se invoca un método que no existe en el objeto sfUser
.
Cuando se inicializa Symfony, también se inicializan todos los plugins que tienen una clase de configuración:
// plugins/sfJobeetPlugin/config/sfJobeetPluginConfiguration.class.php
class sfJobeetPluginConfiguration extends sfPluginConfiguration
{
public function initialize()
{
$this->dispatcher->connect('user.method_not_found', array('JobeetUser', 'methodNotFound'));
}
}
Las notificaciones de los eventos se gestionan mediante el objeto sfEventDispatcher. Registrar un listener (es decir, un método que escucha eventos) es tan sencillo como realizar una llamada al método connect()
. El método connect()
asocia un nombre de evento con un elemento ejecutable de PHP, también llamado "PHP callable".
Nota Un elemento ejecutable de PHP es una variable de PHP que se puede utilizar en la función call_user_func()
y que devuelve true
cuando se pasa a la función is_callable()
. Si el elemento ejecutable es una función, se indica mediante una cadena de texto. Si el elemento ejecutable es el método de una clase u objeto, se indica mediante un array.
El código del ejemplo anterior hace que el objeto myUser
invoque el método estático methodNotFound()
de la clase JobeetUser
cada vez que no se encuentre un método en ese objeto. Después, el método methodNotFound()
se encarga de procesar o ignorar el método que no existe en myUser
.
Elimina todos los métodos de la clase myUser
y crea en su lugar la clase 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');
}
}
Cuando se invoca el método methodNotFound()
, el encargado de notificar los eventos pasa como argumento un objeto de tipo sfEvent.
Si el método existe en la clase JobeetUser
, se invoca y el valor devuelto se devuelve al notificador de eventos. Si no existe el método, Symfony utiliza el siguiente listener registrado para ese evento y si ya no existen más listeners, se lanza una excepción.
El método getSubject()
se puede utilizar para determinar el notificador del evento, que en este caso sería el objeto myUser
.
Como siempre que creas nuevas clases, no te olvides de borrar la cache de Symfony antes de probar la aplicación o antes de ejecutar las pruebas:
$ php symfony cc
20.3.8. Arquitectura por defecto vs. arquitectura de los plugins
Si utilizas la arquitectura de los plugins, puedes organizar tu código de una forma completamente diferente: