Development

/doc/tags/RELEASE_1_1_0_BETA4/book/17-Extending-Symfony.txt

You must first sign up to be able to contribute.

root/doc/tags/RELEASE_1_1_0_BETA4/book/17-Extending-Symfony.txt

Revision 8338, 57.4 kB (checked in by dwhittle, 9 months ago)

1.1: updated docs for bridges and autoloading

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 `frontend/config/factories.yml`
340
341     prod:
342       logger:
343         class:   sfNoLogger
344         param:
345           level:   err
346           loggers: ~
347
348     cli:
349       controller:
350         class: sfConsoleController
351       request:
352         class: sfConsoleRequest
353       response:
354         class: sfConsoleResponse
355
356     test:
357       storage:
358         class: sfSessionTestStorage
359         param:
360           session_path: %SF_TEST_CACHE_DIR%/sessions
361
362     #all:
363     #  controller:
364     #    class: sfFrontWebController
365     #
366     #  request:
367     #    class: sfWebRequest
368     #    param:
369     #      formats:
370     #        txt:  text/plain
371     #        js:   [application/javascript, application/x-javascript, text/javascript]
372     #        css:  text/css
373     #        json: [application/json, application/x-json]
374     #        xml:  [text/xml, application/xml, application/x-xml]
375     #        rdf:  application/rdf+xml
376     #        atom: application/atom+xml
377     #
378     #  response:
379     #    class: sfWebResponse
380     #    param:
381     #      logging: %SF_LOGGING_ENABLED%
382     #      charset: %SF_CHARSET%
383     #
384     #  user:
385     #    class: myUser
386     #    param:
387     #      timeout:         1800
388     #      logging:         %SF_LOGGING_ENABLED%
389     #      use_flash:       true
390     #      default_culture: %SF_DEFAULT_CULTURE%
391     #
392     #  storage:
393     #    class: sfSessionStorage
394     #    param:
395     #      session_name: symfony
396     #
397     #  view_cache:
398     #    class: sfFileCache
399     #    param:
400     #      automatic_cleaning_factor: 0
401     #      cache_dir:                 %SF_TEMPLATE_CACHE_DIR%
402     #      lifetime:                  86400
403     #      prefix:                    %SF_APP_DIR%
404     #
405     #  i18n:
406     #    class: sfI18N
407     #    param:
408     #      source:               XLIFF
409     #      debug:                off
410     #      untranslated_prefix:  "[T]"
411     #      untranslated_suffix:  "[/T]"
412     #      cache:
413     #        class: sfFileCache
414     #        param:
415     #          automatic_cleaning_factor: 0
416     #          cache_dir:                 %SF_I18N_CACHE_DIR%
417     #          lifetime:                  86400
418     #          prefix:                    %SF_APP_DIR%
419     #
420     #  routing:
421     #    class: sfPatternRouting
422     #    param:
423     #      load_configuration: true
424     #      suffix:             .
425     #      default_module:     default
426     #      default_action:     index
427     #      variable_prefixes:  [':']
428     #      segment_separators: ['/', '.']
429     #      variable_regex:     '[\w\d_]+'
430     #      debug:              %SF_DEBUG%
431     #      logging:            %SF_LOGGING_ENABLED%
432     #      cache:
433     #        class: sfFileCache
434     #        param:
435     #          automatic_cleaning_factor: 0
436     #          cache_dir:                 %SF_CONFIG_CACHE_DIR%/routing
437     #          lifetime:                  31556926
438     #          prefix:                    %SF_APP_DIR%
439     #
440     #  logger:
441     #    class: sfAggregateLogger
442     #    param:
443     #      level: debug
444     #      loggers:
445     #        sf_web_debug:
446     #          class: sfWebDebugLogger
447     #          param:
448     #            condition:      %SF_WEB_DEBUG%
449     #            xdebug_logging: true
450     #        sf_file_debug:
451     #          class: sfFileLogger
452     #          param:
453     #            file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%.log
454
455
456 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 `frontend/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.
457
458 Listing 17-14 - Overriding Factories
459
460     [php]
461     // Create a myRequest.class.php in an autoloaded directory,
462     // For instance in frontend/lib/
463     <?php
464
465     class myRequest extends sfRequest
466     {
467       // Your code here
468     }
469
470     // Declare this class as the request factory in factories.yml
471     all:
472       request:
473         class: myRequest
474
475 Integrating with Other Framework's Components
476 ---------------------------------------------
477
478 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 spl autoload integration to take advantage of the autoloading.
479
480 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.
481
482 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`:
483
484     .settings:
485       zend_lib_dir:   /usr/local/zend/library/
486       ez_lib_dir:     /usr/local/ezcomponents/
487       swift_lib_dir:  /usr/local/swiftmailer/
488
489 Then, extend the PHP autoload system by specifying which library to consider when the autoloading fails with symfony. You can do this by registering the autoload classes in the application configuration class (see Chapter 19 for more information):
490
491     [php]
492     class frontendConfiguration extends sfApplicationConfiguration
493     {
494       public function initialize()
495       {
496         parent::initialize(); // load symfony autoloading first
497
498         // Integrate Zend Framework
499         if($sf_zend_lib_dir = sfConfig::get('sf_zend_lib_dir'))
500         {
501           set_include_path($sf_zend_lib_dir.PATH_SEPARATOR.get_include_path());
502
503           require_once($sf_zend_lib_dir.'/Zend/Loader.php');
504           spl_autoload_register(array('Zend_Loader', 'Zend_Loader'));
505         }
506
507         // Integrate eZ Components
508         if($sf_ez_lib_dir = sfConfig::get('sf_ez_lib_dir'))
509         {
510           set_include_path($sf_ez_lib_dir.PATH_SEPARATOR.get_include_path());
511
512           require_once($sf_ez_lib_dir.'/Base/base.php');
513           spl_autoload_register(array('ezcBase', 'autoload'));
514         }
515
516         // Integrate Swift Mailer
517         if($sf_swift_lib_dir = sfConfig::get('sf_swift_lib_dir'))
518         {
519           set_include_path($sf_swift_lib_dir.PATH_SEPARATOR.get_include_path());
520
521           require_once($sf_swift_lib_dir.'/Swift/ClassLoader.php');
522           spl_autoload_register(array('Swift_ClassLoader', 'load'));
523         }
524       }
525     }
526
527 The following describes what will happen when you create a new object of an unloaded class:
528
529   1. The symfony autoloading function (`sfAutoload::autoload()`) first looks for a class in the paths declared in the `autoload.yml` file.
530   2. If no class path is found, the callback methods registered by `spl_autoload_register()` calls will be called one after the other, until one of them returns `true`:
531   3. `Zend_Loader::Zend_Loader()`
532   4. `ezcBase::autoload()`
533   5. `Swift_ClassLoader::load()`
534   6. If these also return `false`, PHP will generate an error.
535
536 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:
537
538     [php]
539     require_once 'Zend/Search/Lucene.php';
540     $doc = new Zend_Search_Lucene_Document();
541     $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
542     ...
543
544 With symfony and spl autoloading, it is simpler. Just write this with out any need for require:
545
546     [php]
547     $doc = new Zend_Search_Lucene_Document(); // The class is autoloaded
548     $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
549     ...
550
551 Plug-Ins
552 --------
553
554 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.
555
556 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.
557
558 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.
559
560 ### Finding Symfony Plug-Ins
561
562 The symfony project website contains a page dedicated to symfony plug-ins. It is in the symfony wiki and accessible with the following URL:
563
564     http://trac.symfony-project.com/wiki/SymfonyPlugins
565
566 Each plug-in listed there has its own page, with detailed installation instructions and documentation.
567
568 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:
569
570   * `sfFeed2Plugin`: Automates the manipulation of RSS and Atom feeds
571   * `sfThumbnailPlugin`: Creates thumbnails--for instance, for uploaded images
572   * `sfMediaLibraryPlugin`: Allows media upload and management, including an extension for rich text editors to allow authoring of images inside rich text
573   * `sfShoppingCartPlugin`: Allows shopping cart management
574   * `sfPagerNavigationPlugin`: Provides classical and Ajax pager controls, based on an `sfPager` object
575   * `sfGuardPlugin`: Provides authentication, authorization, and other user management features above the standard security feature of symfony
576   * `sfPrototypePlugin`: Provides prototype and script.aculo.us JavaScript files as a standalone library
577   * `sfSuperCachePlugin`: Writes pages in cache directory under the web root to allow the web server to serve them as fast as possible
578   * `sfOptimizerPlugin`: Optimizes your application's code to make it execute faster in the production environment (see the next chapter for details)
579   * `sfErrorLoggerPlugin`: Logs every 404 and 500 error in a database and provides an administration module to browse these errors
580   * `sfSslRequirementPlugin`: Provides SSL encryption support for actions
581
582 The wiki also proposes plug-ins designed to extend your Propel objects, also called behaviors. Among them, you will find the following:
583
584   * `sfPropelParanoidBehaviorPlugin`: Disables object deletion and replaces it with the updating of a `deleted_at` column
585   * `sfPropelOptimisticLockBehaviorPlugin`: Implements optimistic locking for Propel objects
586
587 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.
588
589 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.
590
591 ### Installing a Plug-In
592
593 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.
594
595 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.
596
597 #### PEAR Plug-Ins
598
599 Plug-ins listed on the symfony wiki are bundled as PEAR packages attached to a wiki page and made available via the official symfony plugins PEAR channel: `plugins.symfony-project.org`. To install such a plug-in, use the `plugin:install` task with a plugin name, as shown in Listing 17-15.
600
601 Listing 17-15 - Installing a Plug-In from the Official symfony plugins PEAR Channel / Symfony Wiki
602
603     > cd myproject
604     > php symfony plugin:install pluginName
605
606 Alternatively, you can download the plug-in and install it from the disk. In this case, use the path to the package archive, as shown in Listing 17-16.
607
608 Listing 17-16 - Installing a Plug-In from a Downloaded PEAR Package
609
610     > cd myproject
611     > php symfony plugin:install /home/path/to/downloads/pluginName.tgz
612
613 Some plug-ins are hosted on external PEAR channels. Install them with the `plugin:install` task, and don't forget to register the channel and mention the channel name, as shown in Listing 17-17.
614
615 Listing 17-17 - Installing a Plug-In from a PEAR Channel
616
617     > cd myproject
618     > php symfony plugin:add-channel channel.symfony.pear.example.com
619     > php symfony plugin:install --channel=channel.symfony.pear.example.com pluginName
620
621 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 plugins PEAR channel, an external PEAR channel, or a downloaded PEAR package.
622
623 The `plugin:install` task also takes a number of options, as shown on Listing 17-18.
624
625 Listing 17-18 - Installing a Plug-In with some Options
626
627     > php symfony plugin:install --stability=beta pluginName
628     > php symfony plugin:install --release=1.0.3 pluginName
629     > php symfony plugin:install --install-deps pluginName
630
631 >**TIP**
632 >As for every symfony task, you can have a full explanation of the `plugin:install` options and arguments by launching `php symfony help plugin:install`.
633
634 #### Archive Plug-Ins
635
636 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-19. Finally, don't forget to clear the cache.
637
638 Listing 17-19 - Installing a Plug-In from an Archive
639
640     > cd plugins
641     > tar -zxpf myPlugin.tgz
642     > cd ..
643     > ln -sf plugins/myPlugin/web web/myPlugin
644     > php symfony cc
645
646 #### Installing Plug-Ins from a Version Control Repository
647
648 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.
649
650 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-20 demonstrates.
651
652 Listing 17-20 - Installing a Plug-In from a Source Version Repository
653
654     > cd myproject
655     > svn propedit svn:externals plugins
656       pluginName   http://svn.example.com/pluginName/trunk
657     > svn up
658     > php symfony cc
659
660 >**NOTE**
661 >If the plug-in contains a `web/` directory, you must create a symlink to it the same way as for an archive plug-in.
662
663 #### Activating a Plug-In Module
664
665 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/frontend/modules/` directory (to keep them easily upgradeable). They also need to be activated in the `settings.yml` file, as shown in Listing 17-21.
666
667 Listing 17-21 - Activating a Plug-In Module, in `frontend/config/settings.yml`
668
669     all:
670       .settings:
671         enabled_modules:  [default, sfMyPluginModule]
672
673 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.
674
675 >**TIP**
676 >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.
677
678 #### Listing the Installed Plug-Ins
679
680 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-22).
681
682 Listing 17-22 - Listing Installed Plug-Ins
683
684     > cd myproject
685     > php symfony plugin:list
686
687     Installed plugins:
688     sfPrototypePlugin               1.0.0-stable # plugins.symfony-project.com (symfony)
689     sfSuperCachePlugin              1.0.0-stable # plugins.symfony-project.com (symfony)
690     sfThumbnail                     1.1.0-stable # plugins.symfony-project.com (symfony)
691
692 #### Upgrading and Uninstalling Plug-Ins
693
694 To uninstall a PEAR plug-in, call the `plugin:uninstall` task from the root project directory, as shown in Listing 17-23. You must prefix the plug-in name with its installation channel if it's different from the default `symfony` channel (use the `plugin:list` task to determine this channel).
695
696 Listing 17-23 - Uninstalling a Plug-In
697
698     > cd myproject
699     > php symfony plugin:uninstall sfPrototypePlugin
700     > php symfony cc
701
702 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.
703
704 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.
705
706 ### Anatomy of a Plug-In
707
708 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.
709
710 #### Plug-In File Structure
711
712 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-24.
713
714 Listing 17-24 - File Structure of a Plug-In
715
716     pluginName/
717       config/
718         *schema.yml        // Data schema
719         *schema.xml
720         config.php         // Specific plug-in configuration
721       data/
722         generator/
723           sfPropelAdmin
724             */             // Administration generator themes
725               template/
726               skeleton/
727         fixtures/
728           *.yml            // Fixtures files
729       lib/
730         *.php              // Classes
731         helper/
732           *.php            // Helpers
733         model/
734           *.php            // Model classes
735         task/
736           *Task.class.php  // CLI tasks
737       modules/
738         */                 // Modules
739           actions/
740             actions.class.php
741           config/
742             module.yml
743             view.yml
744             security.yml
745           templates/
746             *.php
747           validate/
748             *.yml
749       web/
750         *                  // Assets
751
752 #### Plug-In Abilities
753
754 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:
755
756   * 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-25.
757
758 Listing 17-25 - Example Schema Declaration in a Plug-In, in `myPlugin/config/schema.yml`
759
760     propel:
761       _attributes:    { package: plugins.myPlugin.lib.model }
762       my_plugin_foobar:
763         _attributes:    { phpName: myPluginFoobar }
764           id:
765           name:           { type: varchar, size: 255, index: unique }
766           ...
767
768   * 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.
769   * Fixtures files located in the plug-in `data/fixtures/` directory are processed by the `propel:data-load` task.
770   * Custom classes are autoloaded just like the ones you put in your project `lib/` folders.
771   * 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.
772   * 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.
773   * Tasks are immediately available to the symfony command line as soon as the plug-in is installed. A plugin can either add new tasks, or override an existing one. It is a best practice to use the plug-in name as a namespace for the task. Type `php symfony` to see the list of available tasks, including the ones added by plug-ins.
774   * Modules provide new actions accessible from the outside, provided that you declare them in the `enabled_modules` setting in your application.
775   * 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).
776
777 #### Manual Plug-In Setup
778
779 There are some elements that the `plugin:install` task cannot handle on its own, and which require manual setup during installation:
780
781   * 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-26 for an example).
782   * Custom routing rules have to be added manually to the application `routing.yml`.
783   * Custom filters have to be added manually to the application `filters.yml`.
784   * Custom factories have to be added manually to the application `factories.yml`.
785
786 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.
787
788 #### Customizing a Plug-In for an Application
789
790 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.
791
792 Well-designed plug-ins use settings that can be changed in the application `app.yml`, as Listing 17-26 demonstrates.
793
794 Listing 17-26 - Customizing a Plug-In That Uses the Application Configuration
795
796     [php]
797     // example plug-in code
798     $foo = sfConfig::get('app_my_plugin_foo', 'bar');
799
800     // Change the 'foo' default value ('bar') in the application app.yml
801     all:
802       my_plugin:
803         foo:       barbar
804
805 The module settings and their default values are often described in the plug-in's `README` file.
806
807 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.
808
809 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-27 for an example.
810
811 Listing 17-27 - Customizing a Plug-In Action
812
813     [php]
814     // In myPlugin/modules/mymodule/lib/myPluginmymoduleActions.class.php
815     class myPluginmymoduleActions extends sfActions
816     {
817       public function executeIndex()
818       {
819         // Some code there
820       }
821     }
822
823     // In myPlugin/modules/mymodule/actions/actions.class.php
824
825     require_once dirname(__FILE__).'/../lib/myPluginmymoduleActions.class.php';
826
827     class mymoduleActions extends myPluginmymoduleActions
828     {
829       // Nothing
830     }
831
832     // In frontend/modules/mymodule/actions/actions.class.php
833     class mymoduleActions extends myPluginmymoduleActions
834     {
835       public function executeIndex()
836       {
837         // Override the plug-in code there
838       }
839     }
840
841 >**SIDEBAR**
842 >**New in symfony 1.1**: Customizing the plug-in schema
843 >
844 >When building the model, symfony will look for custom YAML files for each existing schema, including plug-in ones, following this rule:
845 >
846 >Original schema name                   | Custom schema name
847 >-------------------------------------- | ------------------------------
848 >config/schema.yml                      | schema.custom.yml
849 >config/foobar_schema.yml               | foobar_schema.custom.yml
850 >plugins/myPlugin/config/schema.yml     | myPlugin_schema.custom.yml
851 >plugins/myPlugin/config/foo_schema.yml | myPlugin_foo_schema.custom.yml
852 >
853 >Custom schemas will be looked for in the application's and plugins' `config/` directories, so a plugin can override another plugin's schema, and there can be more than one customization per schema.
854 >
855 >Symfony will then merge the two schemas based on each table's `phpName`. The merging process allows for addition or modification of tables, columns, and column attibutes. For instance, the next listing shows how a custom schema can add columns to a table defined in a plug-in schema.
856 >
857 >    # Original schema, in plugins/myPlugin/config/schema.yml
858 >    propel:
859 >      article:
860 >        _attributes:    { phpName: Article }
861 >        title:          varchar(50)
862 >        user_id:        { type: integer }
863 >        created_at:
864 >
865 >    # Custom schema, in myPlugin_schema.custom.yml
866 >    propel:
867 >      article:
868 >        _attributes:    { phpName: Article, package: foo.bar.lib.model }
869 >        stripped_title: varchar(50)
870 >
871 >    # Resulting schema, merged internally and used for model and sql generation
872 >    propel:
873 >      article:
874 >        _attributes:    { phpName: Article, package: foo.bar.lib.model }
875 >        title:          varchar(50)
876 >        user_id:        { type: integer }
877 >        created_at:
878 >        stripped_title: varchar(50)
879 >
880 >As the merging process uses the table's `phpName` as a key, you can even change the name of a plugin table in the database, provided that you keep the same `phpName` in the schema.
881
882 ### How to Write a Plug-In
883
884 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.
885
886 #### File Organization
887
888 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-24. Listing 17-28 shows an example of file structure for an `sfSamplePlugin` plug-in.
889
890 Listing 17-28 - Example List of Files to Package As a Plug-In
891
892     sfSamplePlugin/
893       README
894       LICENSE
895       config/
896         schema.yml
897       data/
898         fixtures/
899           fixtures.yml
900       lib/
901         model/
902           sfSampleFooBar.php
903           sfSampleFooBarPeer.php
904         task/
905           sfSampleTask.class.php
906         validator/
907           sfSampleValidator.class.php
908       modules/
909         sfSampleModule/
910           actions/
911             actions.class.php
912           config/
913             security.yml
914           lib/
915             BasesfSampleModuleActions.class.php
916           templates/
917             indexSuccess.php
918       web/
919         css/
920           sfSampleStyle.css
921         images/
922           sfSampleImage.png
923
924 For authoring, the location of the plug-in directory (`sfSamplePlugin/` in Listing 17-28) is not important. It can be anywhere on the disk.
925
926 >**TIP**
927 >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.
928
929 #### Creating the package.xml File
930
931 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-29.
932
933 Listing 17-29 - Example `package.xml` for a Symfony Plug-In
934
935     [xml]
936     <?xml version="1.0" encoding="UTF-8"?>
937     <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">
938      <name>sfSamplePlugin</name>
939      <channel>plugins.symfony-project.org</channel>
940      <summary>symfony sample plugin</summary>
941      <description>Just a sample plugin to illustrate PEAR packaging</description>
942      <lead>
943       <name>Fabien POTENCIER</name>
944       <user>fabpot</user>
945       <email>fabien.potencier@symfony-project.com</email>
946       <active>yes</active>
947      </lead>
948      <date>2006-01-18</date>
949      <time>15:54:35</time>
950      <version>
951       <release>1.0.0</release>
952       <api>1.0.0</api>
953      </version>
954      <stability>
955       <release>stable</release>
956       <api>stable</api>
957      </stability>
958      <license uri="http://www.symfony-project.org/license">MIT license</license>
959      <notes>-</notes>
960      <contents>
961       <dir name="/">
962        <file role="data" name="README" />
963        <file role="data" name="LICENSE" />
964        <dir name="config">
965         <!-- model -->
966         <file role="data" name="schema.yml" />
967        </dir>
968        <dir name="data">
969         <dir name="fixtures">
970          <!-- fixtures -->
971          <file role="data" name="fixtures.yml" />
972         </dir>
973        </dir>
974        <dir name="lib">
975         <dir name="model">
976          <!-- model classes -->
977          <file role="data" name="sfSampleFooBar.php" />
978          <file role="data" name="sfSampleFooBarPeer.php" />
979         </dir>
980         <dir name="task">
981          <!-- tasks -->
982          <file role="data" name="sfSampleTask.class.php" />
983         </dir>
984         <dir name="validator">
985          <!-- validators -->
986          <file role="data" name="sfSampleValidator.class.php" />
987         </dir>
988        </dir>
989        <dir name="modules">
990         <dir name="sfSampleModule">
991          <file role="data" name="actions/actions.class.php" />
992          <file role="data" name="config/security.yml" />
993          <file role="data" name="lib/BasesfSampleModuleActions.class.php" />
994          <file role="data" name="templates/indexSuccess.php" />
995         </dir>
996        </dir>
997        <dir name="web">
998         <dir name="css">
999          <!-- stylesheets -->
1000          <file role="data" name="sfSampleStyle.css" />
1001         </dir>
1002         <dir name="images">
1003