Development

/doc/tags/RELEASE_1_0_10/17-Extending-Symfony.txt

You must first sign up to be able to contribute.

root/doc/tags/RELEASE_1_0_10/17-Extending-Symfony.txt

Revision 6118, 51.5 kB (checked in by fabien, 1 year ago)

doc: updated symfony project domain name and trac URL

Line 
1 Chapter 17 - Extending Symfony
2 ==============================
3
4 Eventually, you will need to alter symfony's behavior. Whether you need to modify the way a certain class behaves or add your own custom features, the moment will inevitably happen--all clients have specific requirements that no framework can forecast. As a matter of fact, this situation is so common that symfony provides a mechanism to extend existing classes, called a mixin. You can even replace the core symfony classes on your own, using the factories settings. Once you have built an extension, you can easily package it as a plug-in, so that it can be reused in other applications--or by other symfony users.
5
6 Mixins
7 ------
8
9 Among the current limitations of PHP, one of the most annoying is you can't have a class extend more than one class. Another limitation is you can't add new methods to an existing class or override existing methods. To palliate these two limitations and to make the framework truly extendable, symfony introduces a class called `sfMixer`. It is in no way related to cooking devices, but to the concept of mixins found in object-oriented programming. A mixin is a group of methods or functions that can be mixed into a class to extend it.
10
11 ### Understanding Multiple Inheritance
12
13 Multiple inheritance is the ability for a class to extend more than one class and inherit these class properties and methods. Let's consider an example. Imagine a `Story` and a `Book` class, each with its own properties and methods--just like in Listing 17-1.
14
15 Listing 17-1 - Two Example Classes
16
17     [php]
18     class Story
19     {
20       protected $title = '';
21       protected $topic = '';
22       protected $characters = array();
23
24       public function __construct($title = '', $topic = '', $characters = array())
25       {
26         $this->title = $title;
27         $this->topic = $topic;
28         $this->characters = $characters;
29       }
30
31       public function getSummary()
32       {
33         return $this->title.', a story about '.$this->topic;
34       }
35     }
36
37     class Book
38     {
39       protected $isbn = 0;
40
41       function setISBN($isbn = 0)
42       {
43         $this->isbn = $isbn;
44       }
45
46       public function getISBN()
47       {
48         return $this->isbn;
49       }
50     }
51
52 A `ShortStory` class extends `Story`, a `ComputerBook` class extends `Book`, and logically, a `Novel` should extend both `Story` and `Book` and take advantage of all their methods. Unfortunately, this is not possible in PHP. You cannot write the `Novel` declaration as in Listing 17-2.
53
54 Listing 17-2 - Multiple Inheritance Is Not Possible in PHP
55
56     [php]
57     class Novel extends Story, Book
58     {
59     }
60
61     $myNovel = new Novel();
62     $myNovel->getISBN();
63
64 One possibility would be to have Novel implements two interfaces instead of having it extend two classes, but this would prevent you from having the methods actually written in the parent classes.
65
66 ### Mixing Classes
67
68 The `sfMixer` class takes another approach to the problem, taking an existing class and extending it a posteriori, provided that the class contains the proper hooks. The process involves two steps:
69
70   * Declaring a class as extendable
71   * Registering extensions (or mixins), after the class declaration
72
73 Listing 17-3 shows how you would implement the `Novel` class with `sfMixer`.
74
75 Listing 17-3 - Multiple Inheritance Is Possible via `sfMixer`
76
77     [php]
78     class Novel extends Story
79     {
80       public function __call($method, $arguments)
81       {
82         return sfMixer::callMixins();
83       }
84     }
85
86     sfMixer::register('Novel', array('Book', 'getISBN'));
87     $myNovel = new Novel();
88     $myNovel->getISBN();
89
90 One of the classes (`Story`) is chosen as the main parent, in line with PHP's ability to only inherit from one class. The `Novel` class is declared as extendable by the code located in the `__call()` method. The method of the other class (`Book`) is added afterwards to the `Novel` class by a call to `sfMixer::register()`. The next sections will explicitly explain this process.
91
92 When the `getISBN()` method of the `Novel` class is called, everything happens as if the class had been defined as in Listing 17-2--except it's the magic of the `__call()` method and of the `sfMixer` static methods that simulate it. The `getISBN()` method is mixed in the `Novel` class.
93
94 >**SIDEBAR**
95 >When to use mixins
96 >
97 >The symfony mixin mechanism is useful in many cases. Simulating multiple inheritance, as described previously, is just one of them.
98 >
99 >You can use mixins to alter a method after its declaration. For example, when building a graphic library, you will probably implement a `Line` object--representing a line. It will have four attributes (the coordinates for both ends) and a `draw()` method to render itself. A `ColoredLine` should have the same properties and methods, but with an additional attribute, `color`, to specify its color. Furthermore, the `draw()` method of a `ColoredLine` is a little different from the one of a simple `Line`, to use the object's color. You could package the abilities of a graphical element to deal with color into a `ColoredElement` class. This would allow you to reuse the color methods for other graphical elements (`Dot`, `Polygon`, and so on). In this case, the ideal implementation of the `ColoredLine` class would be an extension of the `Line` class, with methods from the `ColoredElement` class mixed in. The final `draw()` method would be a mix between the original one from `Line` and the one from `ColoredElement`.
100 >
101 >Mixins can also be seen as a way to add new methods to an existing class. For instance, the symfony action class, called `sfActions`, is defined in the framework. One of the constraints of PHP is that you cannot change the sfActions definition after its initial declaration. You may want to add a custom method to sfActions in one of your applications only--for instance, to forward a request to a special web service. For that purpose, PHP alone falls short, but the mixin mechanism provides a perfect solution.
102
103 ### Declaring a Class As Extendable
104
105 To declare a class as extendable, you must insert one or several "hooks" into the code, which the `sfMixer` class can later identify. These hooks are calls to the `sfMixer::callMixins()` method. Many of the symfony classes already contain such hooks, including `sfRequest`, `sfResponse`, `sfController`, `sfUser`, `sfAction`, and others.
106
107 The hook can be placed in different parts of the class, according to the desired degree of extensibility:
108
109   * To be able to add new methods to a class, you must insert the hook in the `__call()` method and return its result, as demonstrated in Listing 17-4.
110
111 Listing 17-4 - Giving a Class the Ability to Get New Methods
112
113     [php]
114     class SomeClass
115     {
116       public function __call($method, $arguments)
117       {
118         return sfMixer::callMixins();
119       }
120     }
121
122   * To be able to alter the way an existing method works, you must insert the hook inside the method, as demonstrated in Listing 17-5. The code added by the mixin class will be executed where the hook is placed.
123
124 Listing 17-5 - Giving a Method the Ability to Be Altered
125
126     [php]
127     class SomeOtherClass
128     {
129       public function doThings()
130       {
131         echo "I'm working...";
132         sfMixer::callMixins();
133       }
134     }
135
136 You may want to place more than one hook in a method. In this case, you must name the hooks, so that you can define which hook is to be extended afterwards, as demonstrated in Listing 17-6. A named hook is a call to `callMixins()` with a hook name as a parameter. This name will be used afterwards, when registering a mixin, to tell where in the method the mixin code must be executed.
137
138 Listing 17-6 - A Method Can Contain More Than One Hook, In Which Case They Must Be Named
139
140     [php]
141     class AgainAnotherClass
142     {
143       public function doMoreThings()
144       {
145         echo "I'm ready.";
146         sfMixer::callMixins('beginning');
147         echo "I'm working...";
148         sfMixer::callMixins('end');
149         echo "I'm done.";
150       }
151     }
152
153 Of course, you can combine these techniques to create classes with the ability to be assigned new and extendable methods, as Listing 17-7 demonstrates.
154
155 Listing 17-7 - A Class Can Be Extendable in Various Ways
156
157     [php]
158     class BicycleRider
159     {
160       protected $name = 'John';
161
162       public function getName()
163       {
164         return $this->name;
165       }
166
167       public function sprint($distance)
168       {
169         echo $this->name." sprints ".$distance." meters\n";
170         sfMixer::callMixins(); // The sprint() method is extendable
171       }
172
173       public function climb()
174       {
175         echo $this->name.' climbs';
176         sfMixer::callMixins('slope'); // The climb() method is extendable here
177         echo $this->name.' gets to the top';
178         sfMixer::callMixins('top'); // And also here
179       }
180
181       public function __call($method, $arguments)
182       {
183         return sfMixer::callMixins(); // The BicyleRider class is extendable
184       }
185     }
186
187 >**CAUTION**
188 >Only the classes that are declared as extendable can be extended by `sfMixer`. This means that you cannot use this mechanism to extend a class that didn't "subscribe" to this service.
189
190 ### Registering Extensions
191
192 To register an extension to an existing hook, use the `sfMixer::register()` method. Its first argument is the element to extend, and the second argument is a PHP callable and represents the mixin.
193
194 The format of the first argument depends on what you try to extend:
195
196   * If you extend a class, use the class name.
197   * If you extend a method with an anonymous hook, use the `class:method` pattern.
198   * If you extend a method with a named hook, use the `class:method:hook` pattern.
199
200 Listing 17-8 illustrates this principle by extending the class defined in Listing 17-7. The extended object is automatically passed as first parameter to the mixin methods (except, of course, if the extended method is static). The mixin method also gets access to the parameters of the original method call.
201
202 Listing 17-8 - Registering Extensions
203
204     [php]
205     class Steroids
206     {
207       protected $brand = 'foobar';
208
209       public function partyAllNight($bicycleRider)
210       {
211         echo $bicycleRider->getName()." spends the night dancing.\n";
212         echo "Thanks ".$brand."!\n";
213       }
214
215       public function breakRecord($bicycleRider, $distance)
216       {
217         echo "Nobody ever made ".$distance." meters that fast before!\n";
218       }
219
220       static function pass()
221       {
222         echo " and passes half the peloton.\n";
223       }
224     }
225
226     sfMixer::register('BicycleRider', array('Steroids', 'partyAllNight'));
227     sfMixer::register('BicycleRider:sprint', array('Steroids', 'breakRecord'));
228     sfMixer::register('BicycleRider:climb:slope', array('Steroids', 'pass'));
229     sfMixer::register('BicycleRider:climb:top', array('Steroids', 'pass'));
230
231     $superRider = new BicycleRider();
232     $superRider->climb();
233     => John climbs and passes half the peloton
234     => John gets to the top and passes half the peloton
235     $superRider->sprint(2000);
236     => John sprints 2000 meters
237     => Nobody ever made 2000 meters that fast before!
238     $superRider->partyAllNight();
239     => John spends the night dancing.
240     => Thanks foobar!
241
242 The extension mechanism is not only about adding methods. The partyAllNight() method uses an attribute of the `Steroids` class. This means that when you extend the `BicycleRider` class with a method of the `Steroids` class, you actually create a new `Steroids` instance inside the `BicycleRider` object.
243
244 >**CAUTION**
245 >You cannot add two methods with the same name to an existing class. This is because the `callMixins()` call in the `__call()` methods uses the mixin method name as a key. Also, you cannot add a method to a class that already has a method with the same name, because the mixin mechanism relies on the magic `__call()` method and, in that particular case, it would never be called.
246
247 The second argument of the `register()` call is a PHP callable, so it can be a `class::method` array, or an `object->method` array, or even a function name. See examples in Listing 17-9.
248
249 Listing 17-9 - Any Callable Can Be Registered As a Mixer Extension
250
251     [php]
252     // Use a class method as a callable
253     sfMixer::register('BicycleRider', array('Steroids', 'partyAllNight'));
254
255     // Use an object method as a callable
256     $mySteroids = new Steroids();
257     sfMixer::register('BicycleRider', array($mySteroids, 'partyAllNight'));
258
259     // Use a function as a callable
260     sfMixer::register('BicycleRider', 'die');
261
262 The extension mechanism is dynamic, which means that even if you already instantiated an object, it can take advantage of further extensions in its class. See an example in Listing 17-10.
263
264 Listing 17-10 - The Extension Mechanism Is Dynamic and Can Occur Even After Instantiation
265
266     [php]
267     $simpleRider = new BicycleRider();
268     $simpleRider->sprint(500);
269     => John sprints 500 meters
270     sfMixer::register('BicycleRider:sprint', array('Steroids', 'breakRecord'));
271     $simpleRider->sprint(500);
272     => John sprints 500 meters
273     => Nobody ever made 500 meters that fast before!
274
275 ### Extending with More Precision
276
277 The `sfMixer::callMixins()` instruction is actually a shortcut to something a little bit more elaborate. It automatically loops over the list of registered mixins and calls them one by one, passing to it the current object and the current method parameters. In short, an `sfMixer::callMixins()` call behaves more or less like Listing 17-11.
278
279 Listing 17-11 - `callMixin()` Loops Over the Registered Mixins and Executes Them
280
281     [php]
282     foreach (sfMixer::getCallables($class.':'.$method.':'.$hookName) as $callable)
283     {
284       call_user_func_array($callable, $parameters);
285     }
286
287 If you want to pass other parameters or to do something special with the return value, you can write the foreach loop explicitly instead of using the shortcut method. Look at Listing 17-12 for an example of a mixin more integrated into a class.
288
289 Listing 17-12 - Replacing `callMixin()` by a Custom Loop
290
291     [php]
292     class Income
293     {
294       protected $amount = 0;
295
296       public function calculateTaxes($rate = 0)
297       {
298         $taxes = $this->amount * $rate;
299         foreach (sfMixer::getCallables('Income:calculateTaxes') as $callable)
300         {
301           $taxes += call_user_func($callable, $this->amount, $rate);
302         }
303
304         return $taxes;
305       }
306     }
307
308     class FixedTax
309     {
310       protected $minIncome = 10000;
311       protected $taxAmount = 500;
312
313       public function calculateTaxes($amount)
314       {
315         return ($amount > $this->minIncome) ? $this->taxAmount : 0;
316       }
317     }
318
319     sfMixer::register('Income:calculateTaxes', array('FixedTax', 'calculateTaxes'));
320
321 >**SIDEBAR**
322 >Propel Behaviors
323 >
324 >Propel behaviors, discussed previously in Chapter 8, are a special kind of mixin: They extend Propel-generated objects. Let's look at an example.
325 >
326 >The Propel objects corresponding to the tables of the database all have a delete() method, which deletes the related record from the database. But for an `Invoice` class, for which you can't delete a record, you may want to alter the `delete()` method to be able to keep the record in the database and change the value of an is_deleted attribute to true instead. Usual object retrieval methods (`doSelect()`, `retrieveByPk()`) would only consider the records for which `is_deleted` is false. You would also need to add another method called `forceDelete()`, which would allow you to really delete the record. In fact, all these modifications can be packaged into a new class, called `ParanoidBehavior`. The final `Invoice` class extends the Propel `BaseInvoice` class and has methods of the `ParanoidBehavior` mixed in.
327 >
328 >So a behavior is a mixin on a Propel object. Actually, the term "behavior" in symfony covers one more thing: the fact that the mixin is packaged as a plug-in. The `ParanoidBehavior` class just mentioned corresponds to a real symfony plug-in called `sfPropelParanoidBehaviorPlugin`. Refer to the symfony wiki ([http://trac.symfony-project.com/wiki/sfPropelParanoidBehaviorPlugin](http://trac.symfony-project.com/wiki/sfPropelParanoidBehaviorPlugin)) for details on installation and use of this plug-in.
329 >
330 >One last word about behaviors: To be able to support them, the generated Propel objects must contain quite a number of hooks. These may slow down execution a little and penalize performance if you don't use behaviors. That's why the hooks are not enabled by default. In order to add them and enable behavior support, you must first set the `propel.builder.addBehaviors` property to `true` in the `propel.ini` file and rebuild the model.
331
332 Factories
333 ---------
334
335 A factory is the definition of a class for a certain task. Symfony relies on factories for its core features such as the controller and session capabilities. For instance, when the framework needs to create a new request object, it searches in the factory definition for the name of the class to use for that purpose. The default factory definition for requests is `sfWebRequest`, so symfony creates an object of this class in order to deal with requests. The great advantage of using a factory definition is that it is very easy to alter the core features of the framework: Just change the factory definition, and symfony will use your custom request class instead of its own.
336
337 The factory definitions are stored in the `factories.yml` configuration file. Listing 17-13 shows the default factory definition file. Each definition is made of the name of an autoloaded class and (optionally) a set of parameters. For instance, the session storage factory (set under the `storage:` key) uses a `session_name` parameter to name the cookie created on the client computer to allow persistent sessions.
338
339 Listing 17-13 - Default Factories File, in `myapp/config/factories.yml`
340
341     cli:
342       controller:
343         class: sfConsoleController
344       request:
345         class: sfConsoleRequest
346
347     test:
348       storage:
349         class: sfSessionTestStorage
350
351     #all:
352     #  controller:
353     #    class: sfFrontWebController
354     #
355     #  request:
356     #    class: sfWebRequest
357     #
358     #  response:
359     #    class: sfWebResponse
360     #
361     #  user:
362     #    class: myUser
363     #
364     #  storage:
365     #    class: sfSessionStorage
366     #    param:
367     #      session_name: symfony
368     #
369     #  view_cache:
370     #    class: sfFileCache
371     #    param:
372     #      automaticCleaningFactor: 0
373     #      cacheDir:                %SF_TEMPLATE_CACHE_DIR%
374
375 The best way to change a factory is to create a new class inheriting from the default factory and to add new methods to it. For instance, the user session factory is set to the `myUser` class (located in `myapp/lib/`) and inherits from `sfUser`. Use the same mechanism to take advantage of the existing factories. Listing 17-14 shows an example of a new factory for the request object.
376
377 Listing 17-14 - Overriding Factories
378
379     [php]
380     // Create a myRequest.class.php in an autoloaded directory,
381     // For instance in myapp/lib/
382     <?php
383
384     class myRequest extends sfRequest
385     {
386       // Your code here
387     }
388
389     // Declare this class as the request factory in factories.yml
390     all:
391       request:
392         class: myRequest
393
394 Bridges to Other Framework's Components
395 ---------------------------------------
396
397 If you need capabilities provided by a third-party class, and if you don't want to copy this class in one of the symfony `lib/` dirs, you will probably install it outside of the usual places where symfony looks for files. In that case, using this class will imply a manual `require` in your code, unless you use the symfony bridge to take advantage of the autoloading.
398
399 Symfony doesn't (yet) provide tools for everything. If you need a PDF generator, an API to Google Maps, or a PHP implementation of the Lucene search engine, you will probably need a few libraries from the Zend Framework. If you want to manipulate images directly in PHP, connect to a POP3 account to read e-mails, or design a console interface, you might choose the libraries from eZcomponents. Fortunately, if you define the right settings, the components from both these libraries will work out of the box in symfony.
400
401 The first thing that you need to declare (unless you installed the third-party libraries via PEAR) is the path to the root directory of the libraries. This is to be done in the application `settings.yml`:
402
403     .settings:
404       zend_lib_dir:   /usr/local/zend/library/
405       ez_lib_dir:     /usr/local/ezcomponents/
406
407 Then, extend the autoload routine by specifying which library to consider when the autoloading fails with symfony:
408
409     .settings:
410       autoloading_functions:
411         - [sfZendFrameworkBridge, autoload]
412         - [sfEzComponentsBridge,  autoload]
413
414 Note that this setting is distinct from the rules defined in `autoload.yml` (see Chapter 19 for more information about this file). The `autoloading_functions` setting specifies bridge classes, and `autoload.yml` specifies paths and rules for searching. The following describes what will happen when you create a new object of an unloaded class:
415
416   1. The symfony autoloading function (`sfCore::splAutoload()`) first looks for a class in the paths declared in the `autoload.yml` file.
417   2. If none is found, the callback methods declared in the `sf_autoloading_functions` setting will be called one after the other, until one of them returns `true`:
418   3. `sfZendFrameworkBridge::autoload()`
419   4. `sfEzComponentsBridge::autoload()`
420   5. If these also return false, if you use PHP 5.0.X, symfony will throw an exception saying that the class doesn't exist. Starting with PHP 5.1, the error will be generated by PHP itself.
421
422 This means that the other framework components benefit from the autoload mechanism, and you can use them even more easily than within their own environment. For instance, if you want to use the `Zend_Search` component in the Zend Framework to implement an equivalent of the Lucene search engine in PHP, you have to write this:
423
424     [php]
425     require_once 'Zend/Search/Lucene.php';
426     $doc = new Zend_Search_Lucene_Document();
427     $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
428     ...
429
430 With symfony and the Zend Framework bridge, it is simpler. Just write this:
431
432     [php]
433     $doc = new Zend_Search_Lucene_Document(); // The class is autoloaded
434     $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
435     ...
436
437 The available bridges are stored in the `$sf_symfony_lib_dir/addon/bridge/` directory.
438
439 Plug-Ins
440 --------
441
442 You will probably need to reuse a piece of code that you developed for one of your symfony applications. If you can package this piece of code into a single class, no problem: Drop the class in one of the `lib/` folders of another application and the autoloader will take care of the rest. But if the code is spread across more than one file, such as a complete new theme for the administration generator or a combination of JavaScript files and helpers to automate your favorite visual effect, just copying the files is not the best solution.
443
444 Plug-ins offer a way to package code disseminated in several files and to reuse this code across several projects. Into a plug-in, you can package classes, filters, mixins, helpers, configuration, tasks, modules, schemas and model extensions, fixtures, web assets, etc. Plug-ins are easy to install, upgrade, and uninstall. They can be distributed as a .tgz archive, a PEAR package, or a simple checkout of a code repository. The PEAR packaged plug-ins have the advantage of managing dependencies, being easier to upgrade and automatically discovered. The symfony loading mechanisms take plug-ins into account, and the features offered by a plug-in are available in the project as if the plug-in code was part of the framework.
445
446 So, basically, a plug-in is a packaged extension for a symfony project. With plug-ins, not only can you reuse your own code across applications, but you can also reuse developments made by other contributors and add third-party extensions to the symfony core.
447
448 ### Finding Symfony Plug-Ins
449
450 The symfony project website contains a page dedicated to symfony plug-ins. It is in the symfony wiki and accessible with the following URL:
451
452     http://trac.symfony-project.com/wiki/SymfonyPlugins
453
454 Each plug-in listed there has its own page, with detailed installation instructions and documentation.
455
456 Some of these plug-ins are contributions from the community, and some come from the core symfony developers. Among the latter, you will find the following:
457
458   * `sfFeedPlugin`: Automates the manipulation of RSS and Atom feeds
459   * `sfThumbnailPlugin`: Creates thumbnails--for instance, for uploaded images
460   * `sfMediaLibraryPlugin`: Allows media upload and management, including an extension for rich text editors to allow authoring of images inside rich text
461   * `sfShoppingCartPlugin`: Allows shopping cart management
462   * `sfPagerNavigationPlugin`: Provides classical and Ajax pager controls, based on an `sfPager` object
463   * `sfGuardPlugin`: Provides authentication, authorization, and other user management features above the standard security feature of symfony
464   * `sfPrototypePlugin`: Provides prototype and script.aculo.us JavaScript files as a standalone library
465   * `sfSuperCachePlugin`: Writes pages in cache directory under the web root to allow the web server to serve them as fast as possible
466   * `sfOptimizerPlugin`: Optimizes your application's code to make it execute faster in the production environment (see the next chapter for details)
467   * `sfErrorLoggerPlugin`: Logs every 404 and 500 error in a database and provides an administration module to browse these errors
468   * `sfSslRequirementPlugin`: Provides SSL encryption support for actions
469
470 The wiki also proposes plug-ins designed to extend your Propel objects, also called behaviors. Among them, you will find the following:
471
472   * `sfPropelParanoidBehaviorPlugin`: Disables object deletion and replaces it with the updating of a `deleted_at` column
473   * `sfPropelOptimisticLockBehaviorPlugin`: Implements optimistic locking for Propel objects
474
475 You should regularly check out the symfony wiki, because new plug-ins are added all the time, and they bring very useful shortcuts to many aspects of web application programming.
476
477 Apart from the symfony wiki, the other ways to distribute plug-ins are to propose a plug-ins archive for download, to host them in a PEAR channel, or to store them in a public version control repository.
478
479 ### Installing a Plug-In
480
481 The plug-in installation process differs according to the way it's packaged. Always refer to the included README file and/or installation instructions on the plug-in download page. Also, always clear the symfony cache after installing a plug-in.
482
483 Plug-ins are installed applications on a per-project basis. All the methods described in the following sections result in putting all the files of a plug-in into a `myproject/plugins/pluginName/` directory.
484
485 #### PEAR Plug-Ins
486
487 Plug-ins listed on the symfony wiki are bundled as PEAR packages attached to a wiki page. To install such a plug-in, use the `plugin-install` task with a full URL, as shown in Listing 17-15.
488
489 Listing 17-15 - Installing a Plug-In from the Symfony Wiki
490
491     > cd myproject
492     > php symfony plugin-install http://plugins.symfony-project.com/pluginName
493     > php symfony cc
494
495 Alternatively, you can download the plug-in and install it from the disk. In this case, replace the channel name with the absolute path to the package archive, as shown in Listing 17-16.
496
497 Listing 17-16 - Installing a Plug-In from a Downloaded PEAR Package
498
499     > cd myproject
500     > php symfony plugin-install /home/path/to/downloads/pluginName.tgz
501     > php symfony cc
502
503 Some plug-ins are hosted on PEAR channels. Install them with the `plugin-install` task, and don't forget to mention the channel name, as shown in Listing 17-17.
504
505 Listing 17-17 - Installing a Plug-In from a PEAR Channel
506
507     > cd myproject
508     > php symfony plugin-install channelName/pluginName
509     > php symfony cc
510
511 These three types of installation all use a PEAR package, so the term "PEAR plug-in" will be used indiscriminately to talk about plug-ins installed from the symfony wiki, a PEAR channel, or a downloaded PEAR package.
512
513 #### Archive Plug-Ins
514
515 Some plug-ins come as a simple archive of files. To install those, just unpack the archive into your project's `plugins/` directory. If the plug-in contains a `web/` subdirectory, make a copy or a symlink of this directory into the project's `web/` directory, as demonstrated in Listing 17-18. Finally, don't forget to clear the cache.
516
517 Listing 17-18 - Installing a Plug-In from an Archive
518
519     > cd plugins
520     > tar -zxpf myPlugin.tgz
521     > cd ..
522     > ln -sf plugins/myPlugin/web web/myPlugin
523     > php symfony cc
524
525 #### Installing Plug-Ins from a Version Control Repository
526
527 Plug-ins sometimes have their own source code repository for version control. You can install them by doing a simple checkout in the `plugins/` directory, but this can be problematic if your project itself is under version control.
528
529 Alternatively, you can declare the plug-in as an external dependency so that every update of your project source code also updates the plug-in source code. For instance, Subversion stores external dependencies in the `svn:externals` property. So you can add a plug-in by editing this property and updating your source code afterwards, as Listing 17-19 demonstrates.
530
531 Listing 17-19 - Installing a Plug-In from a Source Version Repository
532
533     > cd myproject
534     > svn propedit svn:externals plugins
535       pluginName   http://svn.example.com/pluginName/trunk
536     > svn up
537     > php symfony cc
538
539 >**NOTE**
540 >If the plug-in contains a `web/` directory, you must create a symlink to it the same way as for an archive plug-in.
541
542 #### Activating a Plug-In Module
543
544 Some plug-ins contain whole modules. The only difference between module plug-ins and classical modules is that module plug-ins don't appear in the `myproject/apps/myapp/modules/` directory (to keep them easily upgradeable). They also need to be activated in the `settings.yml` file, as shown in Listing 17-20.
545
546 Listing 17-20 - Activating a Plug-In Module, in `myapp/config/settings.yml`
547
548     all:
549       .settings:
550         enabled_modules:  [default, sfMyPluginModule]
551
552 This is to avoid a situation where the plug-in module is mistakenly made available for an application that doesn't require it, which could open a security breach. Think about a plug-in that provides `frontend` and `backend` modules. You will need to enable the `frontend` modules only in your `frontend` application, and the `backend` ones only in the `backend` application. This is why plug-in modules are not activated by default.
553
554 >**TIP**
555 >The default module is the only enabled module by default. That's not really a plug-in module, because it resides in the framework, in `$sf_symfony_data_dir/modules/default/`. This is the module that provides the congratulations pages, and the default error pages for 404 and credentials required errors. If you don't want to use the symfony default pages, just remove this module from the `enabled_modules` setting.
556
557 #### Listing the Installed Plug-Ins
558
559 If a glance at your project's `plugins/` directory can tell you which plug-ins are installed, the `plugin-list` task tells you even more: the version number and the channel name of each installed plug-in (see Listing 17-21).
560
561 Listing 17-21 - Listing Installed Plug-Ins
562
563     > cd myproject
564     > php symfony plugin-list
565
566     Installed plugins:
567     sfPrototypePlugin               1.0.0-stable # pear.symfony-project.com (symfony)
568     sfSuperCachePlugin              1.0.0-stable # pear.symfony-project.com (symfony)
569     sfThumbnail                     1.1.0-stable # pear.symfony-project.com (symfony)
570
571 #### Upgrading and Uninstalling Plug-Ins
572
573 To uninstall a PEAR plug-in, call the `plugin-uninstall` task from the root project directory, as shown in Listing 17-22. You must prefix the plug-in name with its installation channel (use the `plugin-list` task to determine this channel).
574
575 Listing 17-22 - Uninstalling a Plug-In
576
577     > cd myproject
578     > php symfony plugin-uninstall pear.symfony-project.com/sfPrototypePlugin
579     > php symfony cc
580
581 >**TIP**
582 >Some channels have an alias. For instance, the `pear.symfony-project.com` channel can also be seen as `symfony`, which means that you can uninstall the `sfPrototypePlugin` as in Listing 17-22 by calling simply `php symfony plugin-uninstall symfony/sfPrototypePlugin`.
583
584 To uninstall an archive plug-in or an SVN plug-in, remove manually the plug-in files from the project `plugins/` and `web/` directories, and clear the cache.
585
586 To upgrade a plug-in, either use the `plugin-upgrade` task (for a PEAR plug-in) or do an `svn update` (if you grabbed the plug-in from a version control repository). Archive plug-ins can't be upgraded easily.
587
588 ### Anatomy of a Plug-In
589
590 Plug-ins are written using the PHP language. If you can understand how an application is organized, you can understand the structure of the plug-ins.
591
592 #### Plug-In File Structure
593
594 A plug-in directory is organized more or less like a project directory. The plug-in files have to be in the right directories in order to be loaded automatically by symfony when needed. Have a look at the plug-in file structure description in Listing 17-23.
595
596 Listing 17-23 - File Structure of a Plug-In
597
598     pluginName/
599       config/
600         *schema.yml        // Data schema
601         *schema.xml
602         config.php         // Specific plug-in configuration
603       data/
604         generator/
605           sfPropelAdmin
606             */             // Administration generator themes
607               templates/
608               skeleton/
609         fixtures/
610           *.yml            // Fixtures files
611         tasks/
612           *.php            // Pake tasks
613       lib/
614         *.php              // Classes
615         helper/
616           *.php            // Helpers
617         model/
618           *.php            // Model classes
619       modules/
620         */                 // Modules
621           actions/
622             actions.class.php
623           config/
624             module.yml
625             view.yml
626             security.yml
627           templates/
628             *.php
629           validate/
630             *.yml
631       web/
632         *                  // Assets
633
634 #### Plug-In Abilities
635
636 Plug-ins can contain a lot of things. Their content is automatically taken into account by your application at runtime and when calling tasks with the command line. But for plug-ins to work properly, you must respect a few conventions:
637
638   * Database schemas are detected by the `propel-` tasks. When you call `propel-build-model` in your project, you rebuild the project model and all the plug-in models with it. Note that a plug-in schema must always have a package attribute under the shape `plugins.pluginName`. `lib.model`, as shown in Listing 17-24.
639
640 Listing 17-24 - Example Schema Declaration in a Plug-In, in `myPlugin/config/schema.yml`
641
642     propel:
643       _attributes:    { package: plugins.myPlugin.lib.model }
644       my_plugin_foobar:
645         _attributes:    { phpName: myPluginFoobar }
646           id:
647           name:           { type: varchar, size: 255, index: unique }
648           ...
649
650   * The plug-in configuration is to be included in the plug-in bootstrap script (`config.php`). This file is executed after the application and project configuration, so symfony is already bootstrapped at that time. You can use this file, for instance, to add directories to the PHP include path or to extend existing classes with mixins.
651   * Fixtures files located in the plug-in `data/fixtures/` directory are processed by the `propel-load-data` task.
652   * Tasks are immediately available to the symfony command line as soon as the plug-in is installed. It is a best practice to prefix the task by something meaningful--for instance, the plug-in name. Type `symfony` to see the list of available tasks, including the ones added by plug-ins.
653   * Custom classes are autoloaded just like the ones you put in your project `lib/` folders.
654   * Helpers are automatically found when you call `use_helper()` in templates. They must be in a` helper/` subdirectory of one of the plug-in's `lib/` directory.
655   * Model classes in `myplugin/lib/model/` specialize the model classes generated by the Propel builder (in `myplugin/lib/model/om/` and `myplugin/lib/model/map/`). They are, of course, autoloaded. Be aware that you cannot override the generated model classes of a plug-in in your own project directories.
656   * Modules provide new actions accessible from the outside, provided that you declare them in the `enabled_modules` setting in your application.
657   * Web assets (images, scripts, style sheets, etc.) are made available to the server. When you install a plug-in via the command line, symfony creates a symlink to the project `web/` directory if the system allows it, or copies the content of the module `web/` directory into the project one. If the plug-in is installed from an archive or a version control repository, you have to copy the plug-in `web/` directory by hand (as the `README` bundled with the plug-in should mention).
658
659 #### Manual Plug-In Setup
660
661 There are some elements that the `plugin-install` task cannot handle on its own, and which require manual setup during installation:
662
663   * Custom application configuration can be used in the plug-in code (for instance, by using `sfConfig::get('app_myplugin_foo')`), but you can't put the default values in an `app.yml` file located in the plug-in `config/` directory. To handle default values, use the second argument of the `sfConfig::get()` method. The settings can still be overridden at the application level (see Listing 17-25 for an example).
664   * Custom routing rules have to be added manually to the application `routing.yml`.
665   * Custom filters have to be added manually to the application `filters.yml`.
666   * Custom factories have to be added manually to the application `factories.yml`.
667
668 Generally speaking, all the configuration that should end up in one of the application configuration files has to be added manually. Plug-ins with such manual setup should embed a `README` file describing installation in detail.
669
670 #### Customizing a Plug-In for an Application
671
672 Whenever you want to customize a plug-in, never alter the code found in the `plugins/` directory. If you do so, you will lose all your modifications when you upgrade the plug-in. For customization needs, plug-ins provide custom settings, and they support overriding.
673
674 Well-designed plug-ins use settings that can be changed in the application `app.yml`, as Listing 17-25 demonstrates.
675
676 Listing 17-25 - Customizing a Plug-In That Uses the Application Configuration
677
678     [php]
679     // example plug-in code
680     $foo = sfConfig::get('app_my_plugin_foo', 'bar');
681
682     // Change the 'foo' default value ('bar') in the application app.yml
683     all:
684       my_plugin:
685         foo:       barbar
686
687 The module settings and their default values are often described in the plug-in's `README` file.
688
689 You can replace the default contents of a plug-in module by creating a module of the same name in your own application. It is not really overriding, since the elements in your application are used instead of the ones of the plug-in. It works fine if you create templates and configuration files of the same name as the ones of the plug-ins.
690
691 On the other hand, if a plug-in wants to offer a module with the ability to override its actions, the `actions.class.php` in the plug-in module must be empty and inherit from an autoloading class, so that the method of this class can be inherited as well by the `actions.class.php` of the application module. See Listing 17-26 for an example.
692
693 Listing 17-26 - Customizing a Plug-In Action
694
695     [php]
696     // In myPlugin/modules/mymodule/lib/myPluginmymoduleActions.class.php
697     class myPluginmymoduleActions extends sfActions
698     {
699       public function executeIndex()
700       {
701         // Some code there
702       }
703     }
704
705     // In myPlugin/modules/mymodule/actions/actions.class.php
706     class mymoduleActions extends myPluginmymoduleActions
707     {
708       // Nothing
709     }
710
711     // In myapp/modules/mymodule/actions/actions.class.php
712     class mymoduleActions extends myPluginmymoduleActions
713     {
714       public function executeIndex()
715       {
716         // Override the plug-in code there
717       }
718     }
719
720 ### How to Write a Plug-In
721
722 Only plug-ins packaged as PEAR packages can be installed with the `plugin-install` task. Remember that such plug-ins can be distributed via the symfony wiki, a PEAR channel, or a simple file download. So if you want to author a plug-in, it is better to publish it as a PEAR package than as a simple archive. In addition, PEAR packaged plug-ins are easier to upgrade, can declare dependencies, and automatically deploy assets in the `web/` directory.
723
724 #### File Organization
725
726 Suppose you have developed a new feature and want to package it as a plug-in. The first step is to organize the files logically so that the symfony loading mechanisms can find them when needed. For that purpose, you have to follow the structure given in Listing 17-23. Listing 17-27 shows an example of file structure for an `sfSamplePlugin` plug-in.
727
728 Listing 17-27 - Example List of Files to Package As a Plug-In
729
730     sfSamplePlugin/
731       README
732       LICENSE
733       config/
734         schema.yml
735       data/
736         fixtures/
737           fixtures.yml
738         tasks/
739           sfSampleTask.php
740       lib/
741         model/
742           sfSampleFooBar.php
743           sfSampleFooBarPeer.php
744         validator/
745           sfSampleValidator.class.php
746       modules/
747         sfSampleModule/
748           actions/
749             actions.class.php
750           config/
751             security.yml
752           lib/
753             BasesfSampleModuleActions.class.php
754           templates/
755             indexSuccess.php
756       web/
757         css/
758           sfSampleStyle.css
759         images/
760           sfSampleImage.png
761
762 For authoring, the location of the plug-in directory (`sfSamplePlugin/` in Listing 17-27) is not important. It can be anywhere on the disk.
763
764 >**TIP**
765 >Take examples of the existing plug-ins and, for your first attempts at creating a plug-in, try to reproduce their naming conventions and file structure.
766
767 #### Creating the package.xml File
768
769 The next step of plug-in authoring is to add a package.xml file at the root of the plug-in directory. The `package.xml` follows the PEAR syntax. Have a look at a typical symfony plug-in `package.xml` in Listing 17-28.
770
771 Listing 17-28 - Example `package.xml` for a Symfony Plug-In
772
773     [xml]
774     <?xml version="1.0" encoding="UTF-8"?>
775     <package packagerversion="1.4.6" 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">
776      <name>sfSamplePlugin</name>
777      <channel>pear.symfony-project.com</channel>
778      <summary>symfony sample plugin</summary>
779      <description>Just a sample plugin to illustrate PEAR packaging</description>
780      <lead>
781       <name>Fabien POTENCIER</name>
782       <user>fabpot</user>
783       <email>fabien.potencier@symfony-project.com</email>
784       <active>yes</active>
785      </lead>
786      <date>2006-01-18</date>
787      <time>15:54:35</time>
788      <version>
789       <release>1.0.0</release>
790       <api>1.0.0</api>
791      </version>
792      <stability>
793       <release>stable</release>
794       <api>stable</api>
795      </stability>
796      <license uri="http://www.symfony-project.org/license">MIT license</license>
797      <notes>-</notes>
798      <contents>
799       <dir name="/">
800        <file role="data" name="README" />
801        <file role="data" name="LICENSE" />
802        <dir name="config">
803         <!-- model -->
804         <file role="data" name="schema.yml" />
805        </dir>
806        <dir name="data">
807         <dir name="fixtures">
808          <!-- fixtures -->
809          <file role="data" name="fixtures.yml" />
810         </dir>
811         <dir name="tasks">
812          <!-- tasks -->
813          <file role="data" name="sfSampleTask.php" />
814         </dir>
815        </dir>
816        <dir name="lib">
817         <dir name="model">
818          <!-- model classes -->
819          <file role="data" name="sfSampleFooBar.php" />
820          <file role="data" name="sfSampleFooBarPeer.php" />
821         </dir>
822         <dir name="validator">
823          <!-- validators ->>
824          <file role="data" name="sfSampleValidator.class.php" />
825         </dir>
826        </dir>
827        <dir name="modules">
828         <dir name="sfSampleModule">
829          <file role="data" name="actions/actions.class.php" />
830          <file role="data" name="config/security.yml" />
831          <file role="data" name="lib/BasesfSampleModuleActions.class.php" />
832          <file role="data" name="templates/indexSuccess.php" />
833         </dir>
834        </dir>
835        <dir name="web">
836         <dir name="css">
837          <!-- stylesheets -->
838          <file role="data" name="sfSampleStyle.css" />
839         </dir>
840         <dir name="images">
841          <!-- images -->
842          <file role="data" name="sfSampleImage.png" />
843         </dir>
844        </dir>
845       </dir>
846      </contents>
847      <dependencies>
848       <required>
849        <php>
850         <min>5.0.0</min>
851        </php>
852        <pearinstaller>
853         <min>1.4.1</min>
854        </pearinstaller>
855        <package>
856         <name>symfony</name>
857         <channel>pear.symfony-project.com</channel>
858         <min>1.0.0</min>
859         <max>1.1.0</max>
860         <exclude>1.1.0</exclude>
861        </package>
862       </required>
863      </dependencies>
864      <phprelease />
865      <changelog />
866     </package>
867
868 The interesting parts here are the `<contents>` and the `<dependencies>` tags, described next. For the rest of the tags, there is nothing specific to symfony, so you can refer to the PEAR online manual ([http://pear.php.net/manual/en/](http://pear.php.net/manual/en/)) for more details about the `package.xml` format.
869
870 #### Contents
871
872 The `<contents>` tag is the place where you must describe the plug-in file structure. This will tell PEAR which files to copy and where. Describe the file structure with `<dir>` and `<file>` tags. All `<file>` tags must have a `role="data"` attribute. The `<contents>` part of Listing 17-28 describes the exact directory structure of Listing 17-27.
873
874 >**NOTE**
875 >The use of `<dir>` tags is not compulsory, since you can use relative paths as `name` values in the `<file>` tags. However, it is recommended so that the `package.xml` file remains readable.
876
877 #### Plug-In Dependencies
878
879 Plug-ins are designed to work with a given set of versions of PHP, PEAR, symfony, PEAR packages, or other plug-ins. Declaring these dependencies in the `<dependencies>` tag tells PEAR to check that the required packages are already installed, and to raise an exception if not.
880
881 You should always declare dependencies on PHP, PEAR, and symfony, at least the ones corresponding to your own installation, as a minimum requirement. If you don't know what to put, add a requirement for PHP 5.0, PEAR 1.4, and symfony 1.0.
882
883 It is also recommended to add a maximum version number of symfony for each plug-in. This will cause an error message when trying to use a plug-in with a more advanced version of the framework, and this will oblige the plug-in author to make sure that the plug-in works correctly with this version before releasing it again. It is better to have an alert and to download an upgrade rather than have a plug-in fail silently.
884
885 #### Building the Plug-In
886
887 The PEAR component has a command (`pear package`) that creates the `.tgz` archive of the package, provided you call the command shown in Listing 17-29 from a directory containing a `package.xml`.
888
889 Listing 17-29 - Packaging a Plug-In As a PEAR Package
890
891     > cd sfSamplePlugin
892     > pear package
893
894     Package sfSamplePlugin-1.0.0.tgz done
895
896 Once your plug-in is built, check that it works by installing it yourself, as shown in Listing 17-30.
897
898 Listing 17-30 - Installing the Plug-In
899
900     > cp sfSamplePlugin-1.0.0.tgz /home/production/myproject/
901     > cd /home/production/myproject/
902     > php symfony plugin-install sfSamplePlugin-1.0.0.tgz
903
904 According to their description in the `<contents>` tag, the packaged files will end up in different directories of your project. Listing 17-31 shows where the files of the `sfSamplePlugin` should end up after installation.
905
906 Listing 17-31 - The Plug-In Files Are Installed on the `plugins/` and `web/` Directories
907
908     plugins/
909       sfSamplePlugin/
910         README
911         LICENSE
912         config/
913           schema.yml
914         data/
915           fixtures/
916             fixtures.yml
917           tasks/
918             sfSampleTask.php
919         lib/
920           model/
921             sfSampleFooBar.php
922             sfSampleFooBarPeer.php
923           validator/
924             sfSampleValidator.class.php
925         modules/
926           sfSampleModule/
927             actions/
928               actions.class.php
929             config/
930               security.yml
931             lib/
932               BasesfSampleModuleActions.class.php
933             templates/
934               indexSuccess.php
935     web/
936       sfSamplePlugin/               ## Copy or symlink, depending on system
937         css/
938           sfSampleStyle.css
939         images/
940           sfSampleImage.png
941
942 Test the way the plug-in behaves in your application. If it works well, you are ready to distribute it across projects--or to contribute it to the symfony community.
943
944 #### Hosting Your Plug-In in the Symfony Project Website
945
946 A symfony plug-in gets the broadest audience when distributed by the `symfony-project.com` website. Even your own plug-ins can be distributed this way, provided that you follow these steps:
947
948   1. Make sure the `README` file describes the way to install and use your plug-in, and that the `LICENSE` file gives the license details. Format your `README` with the Wiki Formatting syntax ([http://trac.symfony-project.com/wiki/WikiFormatting](http://trac.symfony-project.com/wiki/WikiFormatting)).
949   2. Create a PEAR package for your plug-in by calling the `pear package` command, and test it. The PEAR package must be named `sfSamplePlugin-1.0.0.tgz` (1.0.0 is the plug-in version).
950   3. Create a new page on the symfony wiki named `sfSamplePlugin` (`Plugin` is a mandatory suffix). In this page, describe the plug-in usage, the license, the dependencies, and the installation procedure. You can reuse the contents of the plug-in `README` file. Check the existing plug-ins' wiki pages and use them as an example.
951   4. Attach your PEAR package to the wiki page (`sfSamplePlugin-1.0.0.tgz`).
952   5. Add the new plug-in wiki page (`[wiki:sfSamplePlugin]`) to the list of available plug-ins, which is also a wiki page ([http://trac.symfony-project.com/wiki/SymfonyPlugins](http://trac.symfony-project.com/wiki/SymfonyPlugins)).
953
954 If you follow this procedure, users will be able to install your plug-in by simply typing the following command in a project directory:
955
956     > php symfony plugin-install http://plugins.symfony-project.com/sfSamplePlugin
957
958 #### Naming Conventions
959
960 To keep the `plugins/` directory clean, ensure all the plug-in names are in camelCase and end with `Plugin` (for example, `shoppingCartPlugin`, `feedPlugin`, and so on). Before naming your plug-in, check that there is no existing plug-in with the same name.
961
962 >**NOTE**
963 >Plug-ins relying on Propel should contain `Propel` in the name. For instance, an authentication plug-in using the Propel data access objects should be called `sfPropelAuth`.
964
965 Plug-ins should always include a `LICENSE` file describing the conditions of use and the chosen license. You are also advised to add a `README` file to explain the version changes, purpose of the plug-in, its effect, installation and configuration instructions, etc.
966
967 Summary
968 -------
969
970 The symfony classes contain `sfMixer` hooks that give them the ability to be modified at the application level. The mixins mechanism allows multiple inheritance and class overriding at runtime even if the PHP limitations forbid it. So you can easily extend the symfony features, even if you have to modify the core classes for that--the factories configuration is here for that.
971
972 Many such extensions already exist; they are packaged as plug-ins, to be easily installed, upgraded, and uninstalled through the symfony command line. Creating a plug-in is as easy as creating a PEAR package, and provides reusability across applications.
973
974 The symfony wiki contains many plug-ins, and you can even add your own. So now that you know how to do it, we hope that you will enhance the symfony core with a lot of useful extensions!
Note: See TracBrowser for help on using the browser.