Day 20: The Plugins

Yesterday, you learned how to internationalize and localize your symfony
applications. Once again, thanks to the ICU standard and a lot of helpers,
symfony makes this really easy. Until the end of these lines, we will talk about
plugins: what they are, what you can bundle in a plugin, and what they
can be used for.

A symfony plugin offers a way to package and distribute a subset of your project
files. Like a project, a plugin can contain classes, helpers, configuration,
tasks, modules, schemas, and even web assets.

The first usage of plugins is to ease sharing code between your applications, or
even between different projects. Recall that symfony applications only share the
model. Plugins provide a way to share more components between applications.

If you need to reuse the same schema for different projects, or the
same modules, move them to a plugin. As a plugin is just a directory, you can
move it around quite easily by creating a SVN repository and using
svn:externals, or by just copying the files from one project to another.

We call these "private plugins" because their usage is restricted to a single
developer or a company. They are not publicly available.

tip

You can even create a package out of your private plugins, create
your own symfony plugin channel, and install them via the plugin:install
task.

Public plugins are available for the community to download and
install. During
this tutorial, we have used a couple of public plugins: sfGuardPlugin and sfFormExtraPlugin.

They are exactly the same as private plugins. The only difference is that
anybody can install them for their projects. You will learn later on how to
publish and host a public plugin on the symfony website.

There is one more way to think about plugins and how to use them. Forget about
re-usability and sharing. Plugins can be used as a different way to organize
your code. Instead of organizing the files by layers: all models in the
lib/model/ directory, templates in the templates/ directory, ...; the files
are put together by feature: all job files together (the model, modules, and
templates), all CMS files together, and so on.

A plugin is just a directory structure with files organized in a
pre-defined structure, according to the nature of the files. Here, we will move
most of the code we have written for Jobeet in a sfJobeetPlugin. The basic
layout we will use is as follows:

All commands are for Unix like environments. If you use Windows, you can drag
and drop files in the Explorer. And if you use Subversion, or any other tool
to manage your code, use the built-in tools they provide (like svn mv to
move files).

If you were to run the propel:build --model task now, symfony would still
generate the files under lib/model/, which is not what we want. The Propel
output directory can be configured by adding a package option. Open the
schema.yml and add the following configuration:

Now symfony will generate its files under the
plugins/sfJobeetPlugin/lib/model/ directory. The form and filter builders also
take this configuration into account when they generate files.

The propel:build --sql task generates a SQL file to create tables. As the file
is named after the package, remove the current one:

$ rm data/sql/lib.model.schema.sql

Now, if you run propel:build --all --and-load, symfony will generate files
under the plugin lib/model/ directory as expected:

$ php symfony propel:build --all --and-load --no-confirmation

After running the task, check that no lib/model/ directory has been created.
The task has created lib/form/ and lib/filter/ directories, however. They
both include base classes for all Propel forms in your project.

For each module, you also need to change the class name in all
actions.class.php and components.class.php files (for instance, the
affiliateActions class needs to be renamed to sfJobeetAffiliateActions).

The include_partial() and include_component() calls must also be changed in
the following templates:

If you try to browse the Jobeet website now, you will have exceptions telling
you that the modules are not enabled. As plugins are shared amongst all
applications in a project, you need to specifically enable the module you need
for a given application in its settings.yml configuration file:

Even if it is a bit counter-intuitive, a plugin can also contain web assets like
images, stylesheets, and JavaScripts. As we don't want to distribute the Jobeet
plugin, it does not really make sense, but it is possible by creating a
plugins/sfJobeetPlugin/web/ directory.

A plugin's assets must be accessible in the project's web/ directory to be
viewable from a browser. The plugin:publish-assets addresses this by creating
symlinks under Unix system and by copying the files on the Windows platform:

Moving the myUser class methods that deal with job history is a bit more
involved. We could create a JobeetUser class and make myUser inherit from
it. But there is a better way, especially if several plugins want to add new
methods to the class.

Core symfony objects notify events during their life-cycle that you can listen
to. In our case, we need to listen to the user.method_not_found event, which
occurs when an undefined method is called on the sfUser object.

When symfony is initialized, all plugins are also initialized if they have a
plugin configuration class:

Event notifications are managed by
sfEventDispatcher,
the event dispatcher object. Registering a listener is as simple as calling the
connect() method. The connect() method connects an event name to a PHP
callable.

note

A PHP callable is a
PHP variable that can be used by the call_user_func() function and returns
true when passed to the is_callable() function. A string represents a
function, and an array can represent an object method or a class method.

With the above code in place, myUser object will call the static
methodNotFound() method of the JobeetUser class whenever it is unable to
find a method. It is then up to the methodNotFound() method to process the
missing method or not.

Remove all methods from the myUser class and create the JobeetUser class:

When you start implementing a new feature, or if you try to solve a classic web
problem, odds are that someone has already solved the same problem and perhaps
packaged the solution as a symfony plugin. To you look for a public symfony
plugin, go to the plugin section of
the symfony website.

As a plugin is self-contained in a directory, there are several way to install
it:

Using the plugin:install task (it only works if the plugin developer has
created a plugin package and uploaded it on the symfony website)

Downloading the package and manually un-archive it under the plugins/
directory (it also need that the developer has uploaded a package)

Creating a svn:externals in plugins/ for the plugin (it only works if
the plugin developer host its plugin on Subversion)

The last two ways are easy but lack some flexibility. The first way allows you
to install the latest version according to the project symfony version, easily
upgrade to the latest stable release, and to easily manage dependencies between
plugins.

To create a plugin package, you need to add some mandatory files to the plugin
directory structure. First, create a README file at the root of the plugin
directory and explain how to install the plugin, what it provides, and what not.
The README file must be formatted with the
Markdown format. This file
will be used on the symfony website as the main piece of documentation. You can
test the conversion of your README file to HTML by using the
symfony plugin dingus.

sidebar

Plugin Development Tasks

If you find yourself frequently creating private and/or public plugins,
consider taking advantage of some of the tasks in the
sfTaskExtraPlugin.
This plugin, maintained by the core team, includes a number of tasks that
help you streamline the plugin lifecycle:

generate:plugin

plugin:package

You also need to create a LICENSE file. Choosing a license is not an easy
task, but the symfony plugin section only lists plugins that are released under
a license similar to the symfony one (MIT, BSD, LGPL, and PHP). The content of
the LICENSE file will be displayed under the license tab of your plugin's
public page.

The last step is to create a package.xml file at the root of the plugin
directory. This package.xml file follows the
PEAR package syntax.

note

The best way to learn the package.xml syntax is certainly to copy the one
used by an existing plugin.

The package.xml file is composed of several parts as you can see in this
template example:

The <dependencies> tag references all dependencies the plugin might have: PHP,
symfony, and also other plugins. This information is used by the
plugin:install task to install the best plugin version for the project
environment and to also install required plugin dependencies if any.

You should always declare a dependency on symfony, as we have done here.
Declaring a minimum and a maximum version allows the plugin:install to know
what symfony version is mandatory as symfony versions can have slightly
different APIs.

You will automatically become the administrator for the plugin and you will see
an "admin" tab in the interface. In this tab, you will find everything you need
to manage your plugin and upload your packages.

note

The plugin FAQ contains a lot
of useful information for plugin developers.

Creating plugins, and sharing them with the community is one of the best ways to
contribute back to the symfony project. It is so easy, that the symfony plugin
repository is full of useful, fun, but also ridiculous plugins.