Development

/doc/branches/1.1/book/17-Extending-Symfony.txt

You must first sign up to be able to contribute.

root/doc/branches/1.1/book/17-Extending-Symfony.txt

Revision 23981, 61.2 kB (checked in by FabianLange, 4 months ago)

[doc][1.1, 1.2] clarified sentence about routing in plugins (closes #5496)

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 at runtime, beyond simple class inheritance. 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 Events
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 an *event system*, inspired by the Cocoa notification center, and based on the Observer design pattern ([http://en.wikipedia.org/wiki/Observer_pattern](http://en.wikipedia.org/wiki/Observer_pattern)).
10
11 ### Understanding Events
12
13 Some of the symfony classes "notify an event" at various moments of their life. For instance, when the user changes its culture, the user object notifies a `change_culture` event. This event is like a shout in the project's space, saying: "I'm doing that. Do whatever you want about it".
14
15 You can decide to do something special when an event is fired. For instance, you could save the user culture to a database table each time the `change_culture` event is notified, to remember the user prefered culture. In order to do so, you need to *register an event listener*, which is a complicated sentence to say that you must declare a function to be called when the event occurs. Listing 17-1 shows how to register a listener on the user's `change_culture` event.
16
17 Listing 17-1 - Registering an Event Listener
18
19     [php]
20     $dispatcher->connect('user.change_culture', 'changeUserCulture');
21    
22     function changeUserCulture(sfEvent $event)
23     {
24       $user = $event->getSubject();
25       $culture = $event['culture'];
26
27       // do something with the user culture
28     }
29
30 All events and listener registrations are managed by a special object called the *event dispatcher*. This object is available from everywhere in symfony by way of the `sfContext` singleton, and most symfony objects offer a `getEventDispatcher()` method to get direct access to it. Using the dispatcher's `connect()` method, you can register any PHP callable (either a class method or a function) to be called when an event occurs. The first argument of `connect()` is the event identifier, which is a string composed of a namespace and a name. The second argument is a PHP callable.
31
32 Once the function registered in the event dispatcher, it sits and waits until the event is fired. The event dispatcher keeps a record of all event listeners, and knows which ones to call when an event is notified. When calling these methods or functions, the dispatcher passes them an `sfEvent` object as parameter.
33
34 The event object stores information about the notified event. The event notifier can be retrieved thanks to the `getSubject()` method, and the event parameters are accessible by using the event object as an array (for example, `$event['culture']` can be used to retrieve the `culture` parameter passed by `sfUser` when notifying `user.change_culture`).
35
36 To wrap it up, the event system allows you to add abilities to an existing class or modify its methods at runtime, without using inheritance.
37
38 >**NOTE**: In version 1.0, symfony used a similar system but with a different syntax. You may see calls to static methods of an `sfMixer` class to register and notify events in symfony 1.0 code, instead of calls to methods of the event dispatcher. `sfMixer` calls are deprecated, yet they still work in symfony 1.1.
39
40 ### Notifying an Event
41
42 Just like symfony classes notify events, your own classes can offer runtime extensibility and notify events at certain occasions. For instance, let's say that your application requests several third-party web services, and that you have written a `sfRestRequest` class to wrap the REST logic of these requests. A good idea would be to notify an event each time this class makes a new request. This would make the addition of logging or caching capabilities easier in the future. Listing 17-2 shows the code you need to add to an existing `fetch()` method to make it notify an event.
43
44 Listing 17-2 - Notifying an Event
45
46     [php]
47     class sfRestRequest
48     {
49       protected $dispatcher = null;
50
51       public function __construct(sfEventDispatcher $dispatcher)
52       {
53         $this->dispatcher = $dispatcher;
54       }
55      
56       /**
57        * Makes a query to an external web service
58        */
59       public function fetch($uri, $parameters = array())
60       {
61         // Notify the beginning of the fetch process
62         $this->dispatcher->notify(new sfEvent($this, 'rest_request.fetch_prepare', array(
63           'uri'        => $uri,
64           'parameters' => $parameters
65         )));
66        
67         // Make the request and store the result in a $result variable
68         // ...
69        
70         // Notify the end of the fetch process
71         $this->dispatcher->notify(new sfEvent($this, 'rest_request.fetch_success', array(
72           'uri'        => $uri,
73           'parameters' => $parameters,
74           'result'     => $result
75         )));
76        
77         return $result;
78       }
79     }
80
81 The `notify()` method of the event dispatcher expects an `sfEvent` object as argument; this is the very same object that is passed to the event listeners. This object always carries a reference to the notifier (that's why the event instance is initialized with `this`) and an event identifier. Optionally, it accepts an associative array of parameters, giving listeners a way to interact with the notifier's logic.
82
83 >**TIP**
84 >Only the classes that notify events can be extended by way of the event system. So even if you don't know whether you will need to extend a class in the future or not, it is always a good idea to add notifications in the key methods.
85
86 ### Notifying an Event Until a Listener handles it
87
88 By using the `notify()` method, you make sure that all the listeners registered on the notified event are executed. But in some cases, you need to allow a listener to stop the event and prevent further listeners from being notified about it. In this case, you should use `notifyUntil()` instead of `notify()`. The dispatcher will then execute all listeners until one returns `true`, and then stop the event notification. In other terms, `notifyUntil()` is like a shout in the project space saying: "I'm doing that. If somebody cares, then I won't tell anybody else". Listing 17-3 shows how to use this technique in combination with a magic `__call()` method to add methods to an existing class at runtime.
89
90 Listing 17-3 - Notifying an Event Until a Listener Returns True
91
92     [php]
93     class sfRestRequest
94     {
95       // ...
96      
97       public function __call($method, $arguments)
98       {
99         $event = $this->dispatcher->notifyUntil(new sfEvent($this, 'rest_request.method_not_found', array(
100           'method'    => $method,
101           'arguments' => $arguments
102         )));
103         if (!$event->isProcessed())
104         {
105           throw new sfException(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
106         }
107        
108         return $event->getReturnValue();
109       }
110     }
111
112 An event listener registered on the `rest_request.method_not_found` event can test the requested `$method` and decide to handle it, or pass to the next event listener callable. In Listing 17-4, you can see how a third party class can add `put()` and `delete()` methods to the `sfRestRequest` class at runtime with this trick.
113
114 Listing 17-4 - Handling a "Notify Until"-Type Event
115
116     [php]
117     class frontendConfiguration extends sfApplicationConfiguration
118     {
119       public function configure()
120       {
121         // ...
122
123         // Register our listener
124         $this->dispatcher->connect('rest_request.method_not_found', array('sfRestRequestExtension', 'listenToMethodNotFound'));
125       }
126     }
127
128     class sfRestRequestExtension
129     {
130       static public function listenToMethodNotFound(sfEvent $event)
131       {
132         switch ($event['method'])
133         {
134           case 'put':
135             self::put($event->getSubject(), $event['arguments'])
136
137             return true;
138           case 'delete':
139             self::delete($event->getSubject(), $event['arguments'])
140
141             return true;
142           default:
143             return false;
144         }
145       }
146
147       static protected function put($restRequest, $arguments)
148       {
149         // Make a put request and store the result in a $result variable
150         // ...
151        
152         $event->setReturnValue($result);
153       }
154      
155       static protected function delete($restRequest, $arguments)
156       {
157         // Make a delete request and store the result in a $result variable
158         // ...
159        
160         $event->setReturnValue($result);
161       }
162     }
163
164 In practice, `notifyUntil()` offers multiple inheritance capabilities, or rather mixins (the addition of methods from third-party classes to an existing class), to PHP. You can now "inject" new methods to objects that you can't extend by way of inheritance. And this happens at runtime. You are not limited by the Object Oriented capabilities of PHP anymore when you use symfony.
165
166 >**TIP**
167 >As the first listener to catch a `notifyUntil()` event prevents further notification, you may worry about the order of which listeners are executed. This order corresponds to the order in which listeners were registered--first registered, first executed. But in practice, cases when this could be disturbing seldom happen. If you realize that two listeners conflict on a particular event, perhaps your class should notify several events, for instance one at the beginning and one at the end of the method execution. And if you use events to add new methods to an existing class, name your methods wisely so that other attempts at adding methods don't conflict. Prefixing method names with the name of the listener class is a good practice.
168
169 ### Changing the Return Value of a Method
170
171 You can probably imagine how a listener can not only use the information given by an event, but also modify it, to alter the original logic of the notifier. If you want to allow this, you should use the `filter()` method of the event dispatcher rather than `notify()`. All event listeners are then called with two parameters: the event object, and the value to filter. Event listeners must return the value, whether they altered it or not. Listings 17-5 show show `filter()` can be used to filter a response from a web service and escape special characters in that response.
172
173 Listing 17-5 - Notifying and Handling a Filter Event
174
175     [php]
176     class sfRestRequest
177     {
178       // ...
179  
180       /**
181        * Make a query to an external web service
182        */
183       public function fetch($uri, $parameters = array())
184       {
185         // Make the request and store the result in a $result variable
186         // ...
187        
188         // Notify the end of the fetch process
189         return $this->dispatcher->filter(new sfEvent($this, 'rest_request.filter_result', array(
190           'uri'        => $uri,
191           'parameters' => $parameters,
192         ), $result));
193       }
194     }
195
196     // Add escaping to the web service response
197     $dispatcher->connect('rest_request.filter_result', 'rest_htmlspecialchars');
198
199     function rest_htmlspecialchars(sfEvent $event, $result)
200     {
201       return htmlspecialchars($result, ENT_QUOTES, 'UTF-8');
202     }
203
204 ### Built-In Events
205
206 Many of symfony's classes have built-in events, allowing you to extend the framework without necessarily changing the class itself. Table 17-1 lists these events, together with their type and arguments.
207
208 Table 17-1 - Symfony's Events
209
210 | **event namespace** | **event name**     | **type**    | **notifiers**          | **arguments**               |
211 | ------------------- | ------------------ | ----------- | ---------------------- | --------------------------- |
212 | **application**     | log                | notify      | lot of classes         | priority                    |
213 |                     | throw_exception    | notifyUntil | sfException            | -                           |
214 | **command**         | log                | notify      | sfCommand* classes     | priority                    |
215 |                     | pre_command        | notifyUntil | sfTask                 | arguments, options          |
216 |                     | post_command       | notify      | sfTask                 | -                           |
217 |                     | filter_options     | filter      | sfTask                 | command_manager             |
218 | **configuration**   | method_not_found   | notifyUntil | sfProjectConfiguration | method, arguments           |
219 | **component**       | method_not_found   | notifyUntil | sfComponent            | method, arguments           |
220 | **context**         | load_factories     | notify      | sfContext              | -                           |
221 | **controller**      | change_action      | notify      | sfController           | module, action              |
222 |                     | method_not_found   | notifyUntil | sfController           | method, arguments           |
223 |                     | page_not_found     | notify      | sfController           | module, action              |
224 | **plugin**          | pre_install        | notify      | sfPluginManager        | channel, plugin, is_package |
225 |                     | post_install       | notify      | sfPluginManager        | channel, plugin             |
226 |                     | pre_uninstall      | notify      | sfPluginManager        | channel, plugin             |
227 |                     | post_uninstall     | notify      | sfPluginManager        | channel, plugin             |
228 | **request**         | filter_parameters  | filter      | sfWebRequest           | path_info                   |
229 |                     | method_not_found   | notifyUntil | sfRequest              | method, arguments           |
230 | **response**        | method_not_found   | notifyUntil | sfResponse             | method, arguments           |
231 |                     | filter_content     | filter      | sfResponse             | -                           |
232 | **routing**         | load_configuration | notify      | sfRouting              | -                           |
233 | **task**            | cache.clear        | notifyUntil | sfCacheClearTask       | app, type, env              |
234 | **template**        | filter_parameters  | filter      | sfViewParameterHolder  | -                           |
235 | **user**            | change_culture     | notify      | sfUser                 | culture                     |
236 |                     | method_not_found   | notifyUntil | sfUser                 | method, arguments           |
237 | **view**            | configure_format   | notify      | sfView                 | format, response, request   |
238 |                     | method_not_found   | notifyUntil | sfView                 | method, arguments           |
239 | **view.cache**      | filter_content     | filter      | sfViewCacheManager     | response, uri, new          |
240
241 You are free to register event listeners on any of these events. Just make sure that listener callables return a boolean when registered on a `notifyUntil`-type event, and that they return the filtered value when registered on a `filter`-type event.
242
243 Note that the event namespaces don't necessarily match the class role. For instance, all symfony classes notify an `application.log` event when they need something to appear in the log files (and in the web debug toolbar):
244
245     [php]
246     $dispatcher->notify(new sfEvent($this, 'application.log', array($message)));
247
248 Your own classes can do the same and also notify symfony events when it makes sense.
249
250 ### Where To Register Listeners?
251
252 Event listeners need to be registered early in the life of a symfony request. In practice, the right place to register event listeners is in the application configuration class. This class has a reference to the event dispatcher that you can use in the `configure()` method. Listing 17-6 shows how to register a listener on one of the `rest_request` events of the above examples.
253
254 Listing 17-6 - Registering a Listener in the Application Configuration Class, in `apps/frontend/config/ApplicationConfiguration.class.php`
255
256     [php]
257     class frontendConfiguration extends sfApplicationConfiguration
258     {
259       public function configure()
260       {
261         $this->dispatcher->connect('rest_request.method_not_found', array('sfRestRequestExtension', 'listenToMethodNotFound'));
262       }
263     }
264
265 Plug-ins (see below) can register their own event listeners. They should do it in the plug-in's `config/config.php` script, which is executed during application initialization and offers access to the event dispatcher through `$this->dispatcher`.
266
267 >**SIDEBAR**
268 >Propel Behaviors
269 >
270 >Propel behaviors, discussed previously in Chapter 8, use the event system. To be honest, they use symfony 1.0 version of the event system, but that doesn't matter. They package event registering and handling to enable extending Propel-generated objects. Let's look at an example.
271 >
272 >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.
273 >
274 >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.org/wiki/sfPropelParanoidBehaviorPlugin](http://trac.symfony-project.org/wiki/sfPropelParanoidBehaviorPlugin)) for details on installation and use of this plug-in.
275 >
276 >One last word about behaviors: To be able to support them, the generated Propel objects must contain quite a number of event notifications. These may slow down execution a little and penalize performance if you don't use behaviors. That's why the events 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.
277
278 Factories
279 ---------
280
281 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.
282
283 The factory definitions are stored in the `factories.yml` configuration file. Listing 17-7 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.
284
285 Listing 17-7 - Default Factories File, in `frontend/config/factories.yml`
286
287     prod:
288       logger:
289         class:   sfNoLogger
290         param:
291           level:   err
292           loggers: ~
293
294     cli:
295       controller:
296         class: sfConsoleController
297       request:
298         class: sfConsoleRequest
299       response:
300         class: sfConsoleResponse
301
302     test:
303       storage:
304         class: sfSessionTestStorage
305         param:
306           session_path: %SF_TEST_CACHE_DIR%/sessions
307
308     #all:
309     #  controller:
310     #    class: sfFrontWebController
311     #
312     #  request:
313     #    class: sfWebRequest
314     #    param:
315     #      formats:
316     #        txt:  text/plain
317     #        js:   [application/javascript, application/x-javascript, text/javascript]
318     #        css:  text/css
319     #        json: [application/json, application/x-json]
320     #        xml:  [text/xml, application/xml, application/x-xml]
321     #        rdf:  application/rdf+xml
322     #        atom: application/atom+xml
323     #
324     #  response:
325     #    class: sfWebResponse
326     #    param:
327     #      logging: %SF_LOGGING_ENABLED%
328     #      charset: %SF_CHARSET%
329     #
330     #  user:
331     #    class: myUser
332     #    param:
333     #      timeout:         1800
334     #      logging:         %SF_LOGGING_ENABLED%
335     #      use_flash:       true
336     #      default_culture: %SF_DEFAULT_CULTURE%
337     #
338     #  storage:
339     #    class: sfSessionStorage
340     #    param:
341     #      session_name: symfony
342     #
343     #  view_cache:
344     #    class: sfFileCache
345     #    param:
346     #      automatic_cleaning_factor: 0
347     #      cache_dir:                 %SF_TEMPLATE_CACHE_DIR%
348     #      lifetime:                  86400
349     #      prefix:                    %SF_APP_DIR%
350     #
351     #  i18n:
352     #    class: sfI18N
353     #    param:
354     #      source:               XLIFF
355     #      debug:                off
356     #      untranslated_prefix:  "[T]"
357     #      untranslated_suffix:  "[/T]"
358     #      cache:
359     #        class: sfFileCache
360     #        param:
361     #          automatic_cleaning_factor: 0
362     #          cache_dir:                 %SF_I18N_CACHE_DIR%
363     #          lifetime:                  86400
364     #          prefix:                    %SF_APP_DIR%
365     #
366     #  routing:
367     #    class: sfPatternRouting
368     #    param:
369     #      load_configuration: true
370     #      suffix:             .
371     #      default_module:     default
372     #      default_action:     index
373     #      variable_prefixes:  [':']
374     #      segment_separators: ['/', '.']
375     #      variable_regex:     '[\w\d_]+'
376     #      debug:              %SF_DEBUG%
377     #      logging:            %SF_LOGGING_ENABLED%
378     #      cache:
379     #        class: sfFileCache
380     #        param:
381     #          automatic_cleaning_factor: 0
382     #          cache_dir:                 %SF_CONFIG_CACHE_DIR%/routing
383     #          lifetime:                  31556926
384     #          prefix:                    %SF_APP_DIR%
385     #
386     #  logger:
387     #    class: sfAggregateLogger
388     #    param:
389     #      level: debug
390     #      loggers:
391     #        sf_web_debug:
392     #          class: sfWebDebugLogger
393     #          param:
394     #            condition:      %SF_WEB_DEBUG%
395     #            xdebug_logging: true
396     #        sf_file_debug:
397     #          class: sfFileLogger
398     #          param:
399     #            file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%.log
400
401
402 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-8 shows an example of a new factory for the request object.
403
404 Listing 17-8 - Overriding Factories
405
406     [php]
407     // Create a myRequest.class.php in an autoloaded directory,
408     // For instance in frontend/lib/
409     <?php
410
411     class myRequest extends sfRequest
412     {
413       // Your code here
414     }
415
416     // Declare this class as the request factory in factories.yml
417     all:
418       request:
419         class: myRequest
420
421 Integrating with Other Framework's Components
422 ---------------------------------------------
423
424 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.
425
426 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.
427
428 First, you need to declare (unless you installed the third-party libraries via PEAR) the path to the root directory of the libraries in the application's `app.yml`:
429
430     all:
431       zend_lib_dir:   /usr/local/zend/library/
432       ez_lib_dir:     /usr/local/ezcomponents/
433       swift_lib_dir:  /usr/local/swiftmailer/
434
435 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), as in Listing 17-9.
436
437 Listing 17-9 - Extending the Autoloading System To Enable Third Party Components, in apps/frontend/config/ApplicationConfiguration.class.php
438
439     [php]
440     class frontendConfiguration extends sfApplicationConfiguration
441     {
442       public function initialize()
443       {
444         parent::initialize(); // load symfony autoloading first
445
446         // Integrate Zend Framework
447         if ($sf_zend_lib_dir = sfConfig::get('app_zend_lib_dir'))
448         {
449           set_include_path($sf_zend_lib_dir.PATH_SEPARATOR.get_include_path());
450           require_once($sf_zend_lib_dir.'/Zend/Loader.php');
451           spl_autoload_register(array('Zend_Loader', 'autoload'));
452         }
453
454         // Integrate eZ Components
455         if ($sf_ez_lib_dir = sfConfig::get('app_ez_lib_dir'))
456         {
457           set_include_path($sf_ez_lib_dir.PATH_SEPARATOR.get_include_path());
458           require_once($sf_ez_lib_dir.'/Base/base.php');
459           spl_autoload_register(array('ezcBase', 'autoload'));
460         }
461
462         // Integrate Swift Mailer
463         if ($sf_swift_lib_dir = sfConfig::get('app_swift_lib_dir'))
464         {
465           set_include_path($sf_swift_lib_dir.PATH_SEPARATOR.get_include_path());
466           require_once($sf_swift_lib_dir.'/Swift/ClassLoader.php');
467           spl_autoload_register(array('Swift_ClassLoader', 'load'));
468         }
469       }
470     }
471
472 What happens when you create a new object of an unloaded class is simple:
473
474   1. The symfony autoloading function first looks for a class in the paths declared in the `autoload.yml` file.
475   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`. So each of `Zend_Loader::autoload()`, `ezcBase::autoload()`, and `Swift_ClassLoader::load()` are called until one finds the class.
476   3. If these also return `false`, PHP will generate an error.
477
478 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 usually need a `require` statement:
479
480     [php]
481     require_once 'Zend/Search/Lucene.php';
482     $doc = new Zend_Search_Lucene_Document();
483     $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
484     // ...
485
486 With symfony and spl autoloading, it is simpler. You can omit the `require` statement and stop worrying about include paths and class locations:
487
488     [php]
489     $doc = new Zend_Search_Lucene_Document(); // The class is autoloaded
490     $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
491     // ...
492
493 Plug-Ins
494 --------
495
496 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.
497
498 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, event listeners, 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.
499
500 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.
501
502 ### Finding Symfony Plug-Ins
503
504 The symfony project website contains a page dedicated to symfony plug-ins. It is in the symfony wiki and accessible with the following URL:
505
506     http://www.symfony-project.org/plugins/
507
508 Each plug-in listed there has its own page, with detailed installation instructions and documentation.
509
510 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:
511
512   * `sfFeed2Plugin`: Automates the manipulation of RSS and Atom feeds
513   * `sfThumbnailPlugin`: Creates thumbnails--for instance, for uploaded images
514   * `sfMediaLibraryPlugin`: Allows media upload and management, including an extension for rich text editors to allow authoring of images inside rich text
515   * `sfShoppingCartPlugin`: Allows shopping cart management
516   * `sfPagerNavigationPlugin`: Provides classical and Ajax pager controls, based on an `sfPager` object
517   * `sfGuardPlugin`: Provides authentication, authorization, and other user management features above the standard security feature of symfony
518   * `sfPrototypePlugin`: Provides prototype and script.aculo.us JavaScript files as a standalone library
519   * `sfSuperCachePlugin`: Writes pages in cache directory under the web root to allow the web server to serve them as fast as possible
520   * `sfOptimizerPlugin`: Optimizes your application's code to make it execute faster in the production environment (see the next chapter for details)
521   * `sfErrorLoggerPlugin`: Logs every 404 and 500 error in a database and provides an administration module to browse these errors
522   * `sfSslRequirementPlugin`: Provides SSL encryption support for actions
523
524 The wiki also proposes plug-ins designed to extend your Propel objects, also called behaviors. Among them, you will find the following:
525
526   * `sfPropelParanoidBehaviorPlugin`: Disables object deletion and replaces it with the updating of a `deleted_at` column
527   * `sfPropelOptimisticLockBehaviorPlugin`: Implements optimistic locking for Propel objects
528
529 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.
530
531 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.
532
533 ### Installing a Plug-In
534
535 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.
536
537 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.
538
539 #### PEAR Plug-Ins
540
541 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-10.
542
543 Listing 17-10 - Installing a Plug-In from the Official symfony plugins PEAR Channel / Symfony Wiki
544
545     > cd myproject
546     > php symfony plugin:install pluginName
547
548 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-11.
549
550 Listing 17-11 - Installing a Plug-In from a Downloaded PEAR Package
551
552     > cd myproject
553     > php symfony plugin:install /home/path/to/downloads/pluginName.tgz
554
555 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-12.
556
557 Listing 17-12 - Installing a Plug-In from a PEAR Channel
558
559     > cd myproject
560     > php symfony plugin:add-channel channel.symfony.pear.example.com
561     > php symfony plugin:install --channel=channel.symfony.pear.example.com pluginName
562
563 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.
564
565 The `plugin:install` task also takes a number of options, as shown on Listing 17-13.
566
567 Listing 17-13 - Installing a Plug-In with some Options
568
569     > php symfony plugin:install --stability=beta pluginName
570     > php symfony plugin:install --release=1.0.3 pluginName
571     > php symfony plugin:install --install-deps pluginName
572
573 >**TIP**
574 >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`.
575
576 #### Archive Plug-Ins
577
578 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-14. Finally, don't forget to clear the cache.
579
580 Listing 17-14 - Installing a Plug-In from an Archive
581
582     > cd plugins
583     > tar -zxpf myPlugin.tgz
584     > cd ..
585     > ln -sf plugins/myPlugin/web web/myPlugin
586     > php symfony cc
587
588 #### Installing Plug-Ins from a Version Control Repository
589
590 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.
591
592 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-15 demonstrates.
593
594 Listing 17-15 - Installing a Plug-In from a Source Version Repository
595
596     > cd myproject
597     > svn propedit svn:externals plugins
598       pluginName   http://svn.example.com/pluginName/trunk
599     > svn up
600     > php symfony cc
601
602 >**NOTE**
603 >If the plug-in contains a `web/` directory, you must create a symlink to it the same way as for an archive plug-in.
604
605 #### Activating a Plug-In Module
606
607 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-16.
608
609 Listing 17-16 - Activating a Plug-In Module, in `frontend/config/settings.yml`
610
611     all:
612       .settings:
613         enabled_modules:  [default, sfMyPluginModule]
614
615 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.
616
617 >**TIP**
618 >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_lib_dir/controller/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.
619
620 #### Listing the Installed Plug-Ins
621
622 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-17).
623
624 Listing 17-17 - Listing Installed Plug-Ins
625
626     > cd myproject
627     > php symfony plugin:list
628
629     Installed plugins:
630     sfPrototypePlugin               1.0.0-stable # plugins.symfony-project.com (symfony)
631     sfSuperCachePlugin              1.0.0-stable # plugins.symfony-project.com (symfony)
632     sfThumbnail                     1.1.0-stable # plugins.symfony-project.com (symfony)
633
634 #### Upgrading and Uninstalling Plug-Ins
635
636 To uninstall a PEAR plug-in, call the `plugin:uninstall` task from the root project directory, as shown in Listing 17-18. 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).
637
638 Listing 17-18 - Uninstalling a Plug-In
639
640     > cd myproject
641     > php symfony plugin:uninstall sfPrototypePlugin
642     > php symfony cc
643
644 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.
645
646 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.
647
648 ### Anatomy of a Plug-In
649
650 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.
651
652 #### Plug-In File Structure
653
654 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-19.
655
656 Listing 17-19 - File Structure of a Plug-In
657
658     pluginName/
659       config/
660         *schema.yml        // Data schema
661         *schema.xml
662         config.php         // Specific plug-in configuration
663       data/
664         generator/
665           sfPropelAdmin
666             */             // Administration generator themes
667               template/
668               skeleton/
669         fixtures/
670           *.yml            // Fixtures files
671       lib/
672         *.php              // Classes
673         helper/
674           *.php            // Helpers
675         model/
676           *.php            // Model classes
677         task/
678           *Task.class.php  // CLI tasks
679       modules/
680         */                 // Modules
681           actions/
682             actions.class.php
683           config/
684             module.yml
685             view.yml
686             security.yml
687           templates/
688             *.php
689           validate/
690             *.yml
691       web/
692         *                  // Assets
693
694 #### Plug-In Abilities
695
696 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:
697
698   * 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-20.
699
700 Listing 17-20 - Example Schema Declaration in a Plug-In, in `myPlugin/config/schema.yml`
701
702     propel:
703       _attributes:    { package: plugins.myPlugin.lib.model }
704       my_plugin_foobar:
705         _attributes:    { phpName: myPluginFoobar }
706           id:
707           name:           { type: varchar, size: 255, index: unique }
708           ...
709
710   * 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 extend existing classes with event listeners and behaviors.
711   * Fixtures files located in the plug-in `data/fixtures/` directory are processed by the `propel:data-load` task.
712   * Custom classes are autoloaded just like the ones you put in your project `lib/` folders.
713   * 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.
714   * 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.
715   * 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.
716   * Modules provide new actions accessible from the outside, provided that you declare them in the `enabled_modules` setting in your application.
717   * 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).
718
719 >**TIP**: Registering routing rules in a Plug-in
720 >A plug-in can add new rules to the routing system, but it is not recomandable to do it by using a custom `routing.yml` configuration file. This is because the order in which rules are defined is very important, and the simple cascade configuration system of YAML files in symfony would mess this order up. Instead, plug-ins need to register an event listener on the `routing.load_configuration` event and manually prepend rules in the listener:
721 >
722 >     [php]
723 >     // in plugins/myPlugin/config/config.php
724 >     $this->dispatcher->connect('routing.load_configuration', array('myPluginRouting', 'listenToRoutingLoadConfigurationEvent'));
725 >     
726 >     // in plugins/myPlugin/lib/myPluginRouting.php
727 >     class myPluginRouting
728 >     {
729 >       static public function listenToRoutingLoadConfigurationEvent(sfEvent $event)
730 >       {
731 >         $routing = $event->getSubject();
732 >         // add plug-in routing rules on top of the existing ones
733 >         $routing->prependRoute('my_route', '/my_plugin/:action', array('module' => 'myPluginAdministrationInterface'));
734 >       }
735 >     }
736 >
737
738 #### Manual Plug-In Setup
739
740 There are some elements that the `plugin:install` task cannot handle on its own, and which require manual setup during installation:
741
742   * 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).
743   * Custom routing rules have to be added manually to the application `routing.yml`.
744   * Custom filters have to be added manually to the application `filters.yml`.
745   * Custom factories have to be added manually to the application `factories.yml`.
746
747 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.
748
749 #### Customizing a Plug-In for an Application
750
751 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.
752
753 Well-designed plug-ins use settings that can be changed in the application `app.yml`, as Listing 17-21 demonstrates.
754
755 Listing 17-21 - Customizing a Plug-In That Uses the Application Configuration
756
757     [php]
758     // example plug-in code
759     $foo = sfConfig::get('app_my_plugin_foo', 'bar');
760
761     // Change the 'foo' default value ('bar') in the application app.yml
762     all:
763       my_plugin:
764         foo:       barbar
765
766 The module settings and their default values are often described in the plug-in's `README` file.
767
768 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.
769
770 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-22 for an example.
771
772 Listing 17-22 - Customizing a Plug-In Action
773
774     [php]
775     // In myPlugin/modules/mymodule/lib/myPluginmymoduleActions.class.php
776     class myPluginmymoduleActions extends sfActions
777     {
778       public function executeIndex()
779       {
780         // Some code there
781       }
782     }
783
784     // In myPlugin/modules/mymodule/actions/actions.class.php
785
786     require_once dirname(__FILE__).'/../lib/myPluginmymoduleActions.class.php';
787
788     class mymoduleActions extends myPluginmymoduleActions
789     {
790       // Nothing
791     }
792
793     // In frontend/modules/mymodule/actions/actions.class.php
794     class mymoduleActions extends myPluginmymoduleActions
795     {
796       public function executeIndex()
797       {
798         // Override the plug-in code there
799       }
800     }
801
802 >**SIDEBAR**
803 >**New in symfony 1.1**: Customizing the plug-in schema
804 >
805 >When building the model, symfony will look for custom YAML files for each existing schema, including plug-in ones, following this rule:
806 >
807 >Original schema name                   | Custom schema name
808 >-------------------------------------- | ------------------------------
809 >config/schema.yml                      | schema.custom.yml
810 >config/foobar_schema.yml               | foobar_schema.custom.yml
811 >plugins/myPlugin/config/schema.yml     | myPlugin_schema.custom.yml
812 >plugins/myPlugin/config/foo_schema.yml | myPlugin_foo_schema.custom.yml
813 >
814 >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.
815 >
816 >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.
817 >
818 >    # Original schema, in plugins/myPlugin/config/schema.yml
819 >    propel:
820 >      article:
821 >        _attributes:    { phpName: Article }
822 >        title:          varchar(50)
823 >        user_id:        { type: integer }
824 >        created_at:
825 >
826 >    # Custom schema, in myPlugin_schema.custom.yml
827 >    propel:
828 >      article:
829 >        _attributes:    { phpName: Article, package: foo.bar.lib.model }
830 >        stripped_title: varchar(50)
831 >
832 >    # Resulting schema, merged internally and used for model and sql generation
833 >    propel:
834 >      article:
835 >        _attributes:    { phpName: Article, package: foo.bar.lib.model }
836 >        title:          varchar(50)
837 >        user_id:        { type: integer }
838 >        created_at:
839 >        stripped_title: varchar(50)
840 >
841 >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.
842
843 ### How to Write a Plug-In
844
845 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.
846
847 #### File Organization
848
849 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-19. Listing 17-23 shows an example of file structure for an `sfSamplePlugin` plug-in.
850
851 Listing 17-23 - Example List of Files to Package As a Plug-In
852
853     sfSamplePlugin/
854       README
855       LICENSE
856       config/
857         schema.yml
858       data/
859         fixtures/
860           fixtures.yml
861       lib/
862         model/
863           sfSampleFooBar.php
864           sfSampleFooBarPeer.php
865         task/
866           sfSampleTask.class.php
867         validator/
868           sfSampleValidator.class.php
869       modules/
870         sfSampleModule/
871           actions/
872             actions.class.php
873           config/
874             security.yml
875           lib/
876             BasesfSampleModuleActions.class.php
877           templates/
878             indexSuccess.php
879       web/
880         css/
881           sfSampleStyle.css
882         images/
883           sfSampleImage.png
884
885 For authoring, the location of the plug-in directory (`sfSamplePlugin/` in Listing 17-23) is not important. It can be anywhere on the disk.
886
887 >**TIP**
888 >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.
889
890 #### Creating the package.xml File
891
892 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-24.
893
894 Listing 17-24 - Example `package.xml` for a Symfony Plug-In
895
896     [xml]
897     <?xml version="1.0" encoding="UTF-8"?>
898     <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">
899      <name>sfSamplePlugin</name>
900      <channel>plugins.symfony-project.org</channel>
901      <summary>symfony sample plugin</summary>
902      <description>Just a sample plugin to illustrate PEAR packaging</description>
903      <lead>
904       <name>Fabien POTENCIER</name>
905       <user>fabpot</user>
906       <email>fabien.potencier@symfony-project.com</email>
907       <active>yes</active>
908      </lead>
909      <date>2006-01-18</date>
910      <time>15:54:35</time>
911      <version>
912       <release>1.0.0</release>
913       <api>1.0.0</api>
914      </version>
915      <stability>
916       <release>stable</release>
917       <api>stable</api>
918      </stability>
919      <license uri="http://www.symfony-project.org/license">MIT license</license>
920      <notes>-</notes>
921      <contents>
922       <dir name="/">
923        <file role="data" name="README" />
924        <file role="data" name="LICENSE" />
925        <dir name="config">
926         <!-- model -->
927         <file role="data" name="schema.yml" />
928        </dir>
929        <dir name="data">
930         <dir name="fixtures">
931          <!-- fixtures -->
932          <file role="data" name="fixtures.yml" />
933         </dir>
934        </dir>
935        <dir name="lib">
936         <dir name="model">
937          <!-- model classes -->
938          <file role="data" name="sfSampleFooBar.php" />
939          <file role="data" name="sfSampleFooBarPeer.php" />
940         </dir>
941         <dir name="task">
942          <!-- tasks -->
943          <file role="data" name="sfSampleTask.class.php" />
944         </dir>
945         <dir name="validator">
946          <!-- validators -->
947          <file role="data" name="sfSampleValidator.class.php" />
948         </dir>
949        </dir>
950        <dir name="modules">
951         <dir name="sfSampleModule">
952          <file role="data" name="actions/actions.class.php" />
953          <file role="data" name="config/security.yml" />
954          <file role="data" name="lib/BasesfSampleModuleActions.class.php" />
955          <file role="data" name="templates/indexSuccess.php" />
956         </dir>
957        </dir>
958        <dir name="web">
959         <dir name="css">
960          <!-- stylesheets -->
961          <file role="data" name="sfSampleStyle.css" />
962         </dir>
963         <dir name="images">
964          <!-- images -->
965          <file role="data" name="sfSampleImage.png" />
966         </dir>
967        </dir>
968       </dir>
969      </contents>
970      <dependencies>
971       <required>
972        <php>
973         <min>5.1.0</min>
974        </php>
975        <pearinstaller>
976         <min>1.4.1</min>
977        </pearinstaller>
978        <package>
979         <name>symfony</name>
980         <channel>pear.symfony-project.com</channel>
981         <min>1.1.0</min>
982         <max>1.2.0</max>
983         <exclude>1.2.0</exclude>
984        </package>
985       </required>
986      </dependencies>
987      <phprelease />
988      <changelog />
989     </package>
990
991 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.
992
993 #### Contents
994
995 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-24 describes the exact directory structure of Listing 17-23.
996
997 >**NOTE**
998 >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.
999
1000 #### Plug-In Dependencies
1001
1002 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.
1003
1004 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.1, PEAR 1.4, and symfony 1.1.
1005
1006 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.
1007
1008 If you specify plugins as dependencies, users will be able to install your plugin and all its dependencies with a single command:
1009
1010     > php symfony plugin:install --install-deps sfSamplePlugin
1011
1012 #### Building the Plug-In
1013
1014 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-25 from a directory containing a `package.xml`.
1015
1016 Listing 17-25 - Packaging a Plug-In As a PEAR Package
1017
1018     > cd sfSamplePlugin
1019     > pear package
1020
1021     Package sfSamplePlugin-1.0.0.tgz done
1022
1023 Once your plug-in is built, check that it works by installing it yourself, as shown in Listing 17-26.
1024
1025 Listing 17-26 - Installing the Plug-In
1026
1027     > cp sfSamplePlugin-1.0.0.tgz /home/production/myproject/
1028     > cd /home/production/myproject/
1029     > php symfony plugin:install sfSamplePlugin-1.0.0.tgz
1030
1031 According to their description in the `<contents>` tag, the packaged files will end up in different directories of your project. Listing 17-27 shows where the files of the `sfSamplePlugin` should end up after installation.
1032
1033 Listing 17-27 - The Plug-In Files Are Installed on the `plugins/` and `web/` Directories
1034
1035     plugins/
1036       sfSamplePlugin/
1037         README
1038         LICENSE
1039         config/
1040           schema.yml
1041         data/
1042           fixtures/
1043             fixtures.yml
1044         lib/
1045           model/
1046             sfSampleFooBar.php
1047             sfSampleFooBarPeer.php
1048           task/
1049             sfSampleTask.class.php
1050           validator/
1051             sfSampleValidator.class.php
1052         modules/
1053           sfSampleModule/
1054             actions/
1055               actions.class.php
1056             config/
1057               security.yml
1058             lib/
1059               BasesfSampleModuleActions.class.php
1060             templates/
1061               indexSuccess.php
1062     web/
1063       sfSamplePlugin/               ## Copy or symlink, depending on system
1064         css/
1065           sfSampleStyle.css
1066         images/
1067           sfSampleImage.png
1068
1069 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.
1070
1071 #### Hosting Your Plug-In in the Symfony Project Website
1072
1073 A symfony plug-in gets the broadest audience when distributed by the `symfony-project.org` website. Even your own plug-ins can be distributed this way, provided that you follow these steps:
1074
1075   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 Markdown Formatting syntax ([http://daringfireball.net/projects/markdown/syntax](http://daringfireball.net/projects/markdown/syntax)).
1076   2. Create a symfony account (http://www.symfony-project.org/user/new) and create the plugin (http://www.symfony-project.org/plugins/new).
1077   3. 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).
1078   4. Upload your PEAR package (`sfSamplePlugin-1.0.0.tgz`).
1079   5. Your plugin must now appear in the list of plugins ([http://www.symfony-project.org/plugins/](http://www.symfony-project.org/plugins/)).
1080
1081 If you follow this procedure, users will be able to install your plug-in by simply typing the following command in a project directory:
1082
1083     > php symfony plugin:install sfSamplePlugin
1084
1085 #### Naming Conventions
1086
1087 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.
1088
1089 >**NOTE**
1090 >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`.
1091
1092 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.
1093
1094 Summary
1095 -------
1096
1097 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.
1098
1099 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.
1100
1101 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.

The Sensio Labs Network

Since 1998, Sensio Labs has been promoting the Open-Source software movement by providing quality web application development, training, consulting.
Sensio Labs also supports several large Open-Source projects.