[ Index ] |
|
Code source de Symfony 1.0.0 |
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 $amout = 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://www.symfony-project.com/trac/wiki/sfPropelParanoidBehaviorPlugin](http://www.symfony-project.com/trac/wiki/sfPropelParanoidBehaviorPlugin)) for details on installation and use of this plug-in. 329 > 330 >One last word about behaviors: To be able to support them, the generated Propel objects must contain quite a number of hooks. These may slow down execution a little and penalize performance if you don't use behaviors. That's why the hooks are not enabled by default. In order to add them and enable behavior support, you must first set the `propel.builder.addBehaviors` property to `true` in the `propel.ini` file and rebuild the model. 331 332 Factories 333 --------- 334 335 A factory is the definition of a class for a certain task. Symfony relies on factories for its core features such as the controller and session capabilities. For instance, when the framework needs to create a new request object, it searches in the factory definition for the name of the class to use for that purpose. The default factory definition for requests is `sfWebRequest`, so symfony creates an object of this class in order to deal with requests. The great advantage of using a factory definition is that it is very easy to alter the core features of the framework: Just change the factory definition, and symfony will use your custom request class instead of its own. 336 337 The factory definitions are stored in the `factories.yml` configuration file. Listing 17-13 shows the default factory definition file. Each definition is made of the name of an autoloaded class and (optionally) a set of parameters. For instance, the session storage factory (set under the `storage:` key) uses a `session_name` parameter to name the cookie created on the client computer to allow persistent sessions. 338 339 Listing 17-13 - Default Factories File, in `myapp/config/factories.yml` 340 341 cli: 342 controller: 343 class: sfConsoleController 344 request: 345 class: sfConsoleRequest 346 347 test: 348 storage: 349 class: sfSessionTestStorage 350 351 #all: 352 # controller: 353 # class: sfFrontWebController 354 # 355 # request: 356 # class: sfWebRequest 357 # 358 # response: 359 # class: sfWebResponse 360 # 361 # user: 362 # class: myUser 363 # 364 # storage: 365 # class: sfSessionStorage 366 # param: 367 # session_name: symfony 368 # 369 # view_cache: 370 # class: sfFileCache 371 # param: 372 # automaticCleaningFactor: 0 373 # cacheDir: %SF_TEMPLATE_CACHE_DIR% 374 375 The best way to change a factory is to create a new class inheriting from the default factory and to add new methods to it. For instance, the user session factory is set to the `myUser` class (located in `myapp/lib/`) and inherits from `sfUser`. Use the same mechanism to take advantage of the existing factories. Listing 17-14 shows an example of a new factory for the request object. 376 377 Listing 17-14 - Overriding Factories 378 379 [php] 380 // Create a myRequest.class.php in an autoloaded directory, 381 // For instance in myapp/lib/ 382 <?php 383 384 class myRequest extends sfRequest 385 { 386 // Your code here 387 } 388 389 // Declare this class as the request factory in factories.yml 390 all: 391 request: 392 class: myRequest 393 394 Bridges to Other Framework Components 395 ------------------------------------- 396 397 If you need capabilities provided by a third-party class, and if you don't want to copy this class in one of the symfony `lib/` dirs, you will probably install it outside of the usual places where symfony looks for files. In that case, using this class will imply a manual `require` in your code, unless you use the symfony bridge to take advantage of the autoloading. 398 399 Symfony doesn't (yet) provide tools for everything. If you need a PDF generator, an API to Google Maps, or a PHP implementation of the Lucene search engine, you will probably need a few libraries from the Zend Framework. If you want to manipulate images directly in PHP, connect to a POP3 account to read e-mails, or design a console interface, you might choose the libraries from eZcomponents. Fortunately, if you define the right settings, the components from both these libraries will work out of the box in symfony. 400 401 The first thing that you need to declare (unless you installed the third-party libraries via PEAR) is the path to the root directory of the libraries. This is to be done in the application `settings.yml`: 402 403 .settings: 404 zend_lib_dir: /usr/local/zend/library/ 405 ez_lib_dir: /usr/local/ezcomponents/ 406 407 Then, extend the autoload routine by specifying which library to consider when the autoloading fails with symfony: 408 409 .settings: 410 autoloading_functions: 411 - [sfZendFrameworkBridge, autoload] 412 - [sfEzComponentsBridge, autoload] 413 414 Note that this setting is distinct from the rules defined in `autoload.yml` (see Chapter 19 for more information about this file). The `autoloading_functions` setting specifies bridge classes, and `autoload.yml` specifies paths and rules for searching. The following describes what will happen when you create a new object of an unloaded class: 415 416 1. The symfony autoloading function (`sfCore::splAutoload()`) first looks for a class in the paths declared in the `autoload.yml` file. 417 2. If none is found, the callback methods declared in the `sf_autoloading_functions` setting will be called one after the other, until one of them returns `true`: 418 3. `sfZendFrameworkBridge::autoload()` 419 4. `sfEzComponentsBridge::autoload()` 420 5. If these also return false, if you use PHP 5.0.X, symfony will throw an exception saying that the class doesn't exist. Starting with PHP 5.1, the error will be generated by PHP itself. 421 422 This means that the other framework components benefit from the autoload mechanism, and you can use them even more easily than within their own environment. For instance, if you want to use the `Zend_Search` component in the Zend Framework to implement an equivalent of the Lucene search engine in PHP, you have to write this: 423 424 [php] 425 require_once 'Zend/Search/Lucene.php'; 426 $doc = new Zend_Search_Lucene_Document(); 427 $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl)); 428 ... 429 430 With symfony and the Zend Framework bridge, it is simpler. Just write this: 431 432 [php] 433 $doc = new Zend_Search_Lucene_Document(); // The class is autoloaded 434 $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl)); 435 ... 436 437 The available bridges are stored in the `$sf_symfony_lib_dir/addon/bridge/` directory. 438 439 Plug-Ins 440 -------- 441 442 You will probably need to reuse a piece of code that you developed for one of your symfony applications. If you can package this piece of code into a single class, no problem: Drop the class in one of the `lib/` folders of another application and the autoloader will take care of the rest. But if the code is spread across more than one file, such as a complete new theme for the administration generator or a combination of JavaScript files and helpers to automate your favorite visual effect, just copying the files is not the best solution. 443 444 Plug-ins offer a way to package code disseminated in several files and to reuse this code across several projects. Into a plug-in, you can package classes, filters, mixins, helpers, configuration, tasks, modules, schemas and model extensions, fixtures, web assets, etc. Plug-ins are easy to install, upgrade, and uninstall. They can be distributed as a .tgz archive, a PEAR package, or a simple checkout of a code repository. The PEAR packaged plug-ins have the advantage of managing dependencies, being easier to upgrade and automatically discovered. The symfony loading mechanisms take plug-ins into account, and the features offered by a plug-in are available in the project as if the plug-in code was part of the framework. 445 446 So, basically, a plug-in is a packaged extension for a symfony project. With plug-ins, not only can you reuse your own code across applications, but you can also reuse developments made by other contributors and add third-party extensions to the symfony core. 447 448 ### Finding Symfony Plug-Ins 449 450 The symfony project website contains a page dedicated to symfony plug-ins. It is in the symfony wiki and accessible with the following URL: 451 452 http://www.symfony-project.com/trac/wiki/SymfonyPlugins 453 454 Each plug-in listed there has its own page, with detailed installation instructions and documentation. 455 456 Some of these plug-ins are contributions from the community, and some come from the core symfony developers. Among the latter, you will find the following: 457 458 * `sfFeedPlugin`: Automates the manipulation of RSS and Atom feeds 459 * `sfThumbnailPlugin`: Creates thumbnails--for instance, for uploaded images 460 * `sfMediaLibraryPlugin`: Allows media upload and management, including an extension for rich text editors to allow authoring of images inside rich text 461 * `sfShoppingCartPlugin`: Allows shopping cart management 462 * `sfPagerNavigationPlugin`: Provides classical and Ajax pager controls, based on an `sfPager` object 463 * `sfGuardPlugin`: Provides authentication, authorization, and other user management features above the standard security feature of symfony 464 * `sfPrototypePlugin`: Provides prototype and script.aculo.us JavaScript files as a standalone library 465 * `sfSuperCachePlugin`: Writes pages in cache directory under the web root to allow the web server to serve them as fast as possible 466 * `sfOptimizerPlugin`: Optimizes your application's code to make it execute faster in the production environment (see the next chapter for details) 467 * `sfErrorLoggerPlugin`: Logs every 404 and 500 error in a database and provides an administration module to browse these errors 468 * `sfSslRequirementPlugin`: Provides SSL encryption support for actions 469 470 The wiki also proposes plug-ins designed to extend your Propel objects, also called behaviors. Among them, you will find the following: 471 472 * `sfPropelParanoidBehaviorPlugin`: Disables object deletion and replaces it with the updating of a `deleted_at` column 473 * `sfPropelOptimisticLockBehaviorPlugin`: Implements optimistic locking for Propel objects 474 475 You should regularly check out the symfony wiki, because new plug-ins are added all the time, and they bring very useful shortcuts to many aspects of web application programming. 476 477 Apart from the symfony wiki, the other ways to distribute plug-ins are to propose a plug-ins archive for download, to host them in a PEAR channel, or to store them in a public version control repository. 478 479 ### Installing a Plug-In 480 481 The plug-in installation process differs according to the way it's packaged. Always refer to the included README file and/or installation instructions on the plug-in download page. Also, always clear the symfony cache after installing a plug-in. 482 483 Plug-ins are installed applications on a per-project basis. All the methods described in the following sections result in putting all the files of a plug-in into a `myproject/plugins/pluginName/` directory. 484 485 #### PEAR Plug-Ins 486 487 Plug-ins listed on the symfony wiki are bundled as PEAR packages attached to a wiki page. To install such a plug-in, use the `plugin-install` task with a full URL, as shown in Listing 17-15. 488 489 Listing 17-15 - Installing a Plug-In from the Symfony Wiki 490 491 > cd myproject 492 > php symfony plugin-install http://plugins.symfony-project.com/pluginName 493 > php symfony cc 494 495 Alternatively, you can download the plug-in and install it from the disk. In this case, replace the channel name with the absolute path to the package archive, as shown in Listing 17-16. 496 497 Listing 17-16 - Installing a Plug-In from a Downloaded PEAR Package 498 499 > cd myproject 500 > php symfony plugin-install /home/path/to/downloads/pluginName.tgz 501 > php symfony cc 502 503 Some plug-ins are hosted on PEAR channels. Install them with the `plugin-install` task, and don't forget to mention the channel name, as shown in Listing 17-17. 504 505 Listing 17-17 - Installing a Plug-In from a PEAR Channel 506 507 > cd myproject 508 > php symfony plugin-install channelName/pluginName 509 > php symfony cc 510 511 These three types of installation all use a PEAR package, so the term "PEAR plug-in" will be used indiscriminately to talk about plug-ins installed from the symfony wiki, a PEAR channel, or a downloaded PEAR package. 512 513 #### Archive Plug-Ins 514 515 Some plug-ins come as a simple archive of files. To install those, just unpack the archive into your project's `plugins/` directory. If the plug-in contains a `web/` subdirectory, make a copy or a symlink of this directory into the project's `web/` directory, as demonstrated in Listing 17-18. Finally, don't forget to clear the cache. 516 517 Listing 17-18 - Installing a Plug-In from an Archive 518 519 > cd plugins 520 > tar -zxpf myPlugin.tgz 521 > cd .. 522 > ln -sf plugins/myPlugin/web web/myPlugin 523 > php symfony cc 524 525 #### Installing Plug-Ins from a Version Control Repository 526 527 Plug-ins sometimes have their own source code repository for version control. You can install them by doing a simple checkout in the `plugins/` directory, but this can be problematic if your project itself is under version control. 528 529 Alternatively, you can declare the plug-in as an external dependency so that every update of your project source code also updates the plug-in source code. For instance, Subversion stores external dependencies in the `svn:externals` property. So you can add a plug-in by editing this property and updating your source code afterwards, as Listing 17-19 demonstrates. 530 531 Listing 17-19 - Installing a Plug-In from a Source Version Repository 532 533 > cd myproject 534 > svn propedit svn:externals plugins 535 pluginName http://svn.example.com/pluginName/trunk 536 > svn up 537 > php symfony cc 538 539 >**NOTE** 540 >If the plug-in contains a `web/` directory, you must create a symlink to it the same way as for an archive plug-in. 541 542 #### Activating a Plug-In Module 543 544 Some plug-ins contain whole modules. The only difference between module plug-ins and classical modules is that module plug-ins don't appear in the `myproject/apps/myapp/modules/` directory (to keep them easily upgradeable). They also need to be activated in the `settings.yml` file, as shown in Listing 17-20. 545 546 Listing 17-20 - Activating a Plug-In Module, in `myapp/config/settings.yml` 547 548 all: 549 .settings: 550 enabled_modules: [default, sfMyPluginModule] 551 552 This is to avoid a situation where the plug-in module is mistakenly made available for an application that doesn't require it, which could open a security breach. Think about a plug-in that provides `frontend` and `backend` modules. You will need to enable the `frontend` modules only in your `frontend` application, and the `backend` ones only in the `backend` application. This is why plug-in modules are not activated by default. 553 554 >**TIP** 555 >The default module is the only enabled module by default. That's not really a plug-in module, because it resides in the framework, in `$sf_symfony_data_dir/modules/default/`. This is the module that provides the congratulations pages, and the default error pages for 404 and credentials required errors. If you don't want to use the symfony default pages, just remove this module from the `enabled_modules` setting. 556 557 #### Listing the Installed Plug-Ins 558 559 If a glance at your project's `plugins/` directory can tell you which plug-ins are installed, the `plugin-list` task tells you even more: the version number and the channel name of each installed plug-in (see Listing 17-21). 560 561 Listing 17-21 - Listing Installed Plug-Ins 562 563 > cd myproject 564 > php symfony plugin-list 565 566 Installed plugins: 567 sfPrototypePlugin 1.0.0-stable # pear.symfony-project.com (symfony) 568 sfSuperCachePlugin 1.0.0-stable # pear.symfony-project.com (symfony) 569 sfThumbnail 1.1.0-stable # pear.symfony-project.com (symfony) 570 571 #### Upgrading and Uninstalling Plug-Ins 572 573 To uninstall a PEAR plug-in, call the `plugin-uninstall` task from the root project directory, as shown in Listing 17-22. You must prefix the plug-in name with its installation channel (use the `plugin-list` task to determine this channel). 574 575 Listing 17-22 - Uninstalling a Plug-In 576 577 > cd myproject 578 > php symfony plugin-uninstall pear.symfony-project.com/sfPrototypePlugin 579 > php symfony cc 580 581 >**TIP** 582 >Some channels have an alias. For instance, the `pear.symfony-project.com` channel can also be seen as `symfony`, which means that you can uninstall the `sfPrototypePlugin` as in Listing 17-22 by calling simply `php symfony plugin-uninstall symfony/sfPrototypePlugin`. 583 584 To uninstall an archive plug-in or an SVN plug-in, remove manually the plug-in files from the project `plugins/` and `web/` directories, and clear the cache. 585 586 To upgrade a plug-in, either use the `plugin-upgrade` task (for a PEAR plug-in) or do an `svn update` (if you grabbed the plug-in from a version control repository). Archive plug-ins can't be upgraded easily. 587 588 ### Anatomy of a Plug-In 589 590 Plug-ins are written using the PHP language. If you can understand how an application is organized, you can understand the structure of the plug-ins. 591 592 #### Plug-In File Structure 593 594 A plug-in directory is organized more or less like a project directory. The plug-in files have to be in the right directories in order to be loaded automatically by symfony when needed. Have a look at the plug-in file structure description in Listing 17-23. 595 596 Listing 17-23 - File Structure of a Plug-In 597 598 pluginName/ 599 config/ 600 *schema.yml // Data schema 601 *schema.xml 602 config.php // Specific plug-in configuration 603 data/ 604 generator/ 605 sfPropelAdmin 606 */ // Administration generator themes 607 templates/ 608 skeleton/ 609 fixtures/ 610 *.yml // Fixtures files 611 tasks/ 612 *.php // Pake tasks 613 lib/ 614 *.php // Classes 615 helper/ 616 *.php // Helpers 617 model/ 618 *.php // Model classes 619 modules/ 620 */ // Modules 621 actions/ 622 actions.class.php 623 config/ 624 module.yml 625 view.yml 626 security.yml 627 templates/ 628 *.php 629 validate/ 630 *.yml 631 web/ 632 * // Assets 633 634 #### Plug-In Abilities 635 636 Plug-ins can contain a lot of things. Their content is automatically taken into account by your application at runtime and when calling tasks with the command line. But for plug-ins to work properly, you must respect a few conventions: 637 638 * Database schemas are detected by the `propel-` tasks. When you call `propel-build-model` in your project, you rebuild the project model and all the plug-in models with it. Note that a plug-in schema must always have a package attribute under the shape `plugins.pluginName`. `lib.model`, as shown in Listing 17-24. 639 640 Listing 17-24 - Example Schema Declaration in a Plug-In, in `myPlugin/config/schema.yml` 641 642 propel: 643 _attributes: { package: plugins.myPlugin.lib.model } 644 my_plugin_foobar: 645 _attributes: { phpName: myPluginFoobar } 646 id: 647 name: { type: varchar, size: 255, index: unique } 648 ... 649 650 * The plug-in configuration is to be included in the plug-in bootstrap script (`config.php`). This file is executed after the application and project configuration, so symfony is already bootstrapped at that time. You can use this file, for instance, to add directories to the PHP include path or to extend existing classes with mixins. 651 * Fixtures files located in the plug-in `data/fixtures/` directory are processed by the `propel-load-data` task. 652 * Tasks are immediately available to the symfony command line as soon as the plug-in is installed. It is a best practice to prefix the task by something meaningful--for instance, the plug-in name. Type `symfony` to see the list of available tasks, including the ones added by plug-ins. 653 * Custom classes are autoloaded just like the ones you put in your project `lib/` folders. 654 * Helpers are automatically found when you call `use_helper()` in templates. They must be in a` helper/` subdirectory of one of the plug-in's `lib/` directory. 655 * Model classes in `myplugin/lib/model/` specialize the model classes generated by the Propel builder (in `myplugin/lib/model/om/` and `myplugin/lib/model/map/`). They are, of course, autoloaded. Be aware that you cannot override the generated model classes of a plug-in in your own project directories. 656 * Modules provide new actions accessible from the outside, provided that you declare them in the `enabled_modules` setting in your application. 657 * Web assets (images, scripts, style sheets, etc.) are made available to the server. When you install a plug-in via the command line, symfony creates a symlink to the project `web/` directory if the system allows it, or copies the content of the module `web/` directory into the project one. If the plug-in is installed from an archive or a version control repository, you have to copy the plug-in `web/` directory by hand (as the `README` bundled with the plug-in should mention). 658 659 #### Manual Plug-In Setup 660 661 There are some elements that the `plugin-install` task cannot handle on its own, and which require manual setup during installation: 662 663 * Custom application configuration can be used in the plug-in code (for instance, by using `sfConfig::get('app_myplugin_foo')`), but you can't put the default values in an `app.yml` file located in the plug-in `config/` directory. To handle default values, use the second argument of the `sfConfig::get()` method. The settings can still be overridden at the application level (see Listing 17-25 for an example). 664 * Custom routing rules have to be added manually to the application `routing.yml`. 665 * Custom filters have to be added manually to the application `filters.yml`. 666 * Custom factories have to be added manually to the application `factories.yml`. 667 668 Generally speaking, all the configuration that should end up in one of the application configuration files has to be added manually. Plug-ins with such manual setup should embed a `README` file describing installation in detail. 669 670 #### Customizing a Plug-In for an Application 671 672 Whenever you want to customize a plug-in, never alter the code found in the `plugins/` directory. If you do so, you will lose all your modifications when you upgrade the plug-in. For customization needs, plug-ins provide custom settings, and they support overriding. 673 674 Well-designed plug-ins use settings that can be changed in the application `app.yml`, as Listing 17-25 demonstrates. 675 676 Listing 17-25 - Customizing a Plug-In That Uses the Application Configuration 677 678 [php] 679 // example plug-in code 680 $foo = sfConfig::get('app_my_plugin_foo', 'bar'); 681 682 // Change the 'foo' default value ('bar') in the application app.yml 683 all: 684 my_plugin: 685 foo: barbar 686 687 The module settings and their default values are often described in the plug-in's `README` file. 688 689 You can replace the default contents of a plug-in module by creating a module of the same name in your own application. It is not really overriding, since the elements in your application are used instead of the ones of the plug-in. It works fine if you create templates and configuration files of the same name as the ones of the plug-ins. 690 691 On the other hand, if a plug-in wants to offer a module with the ability to override its actions, the `actions.class.php` in the plug-in module must be empty and inherit from an autoloading class, so that the method of this class can be inherited as well by the `actions.class.php` of the application module. See Listing 17-26 for an example. 692 693 Listing 17-26 - Customizing a Plug-In Action 694 695 [php] 696 // In myPlugin/modules/mymodule/lib/myPluginmymoduleActions.class.php 697 class myPluginmymoduleActions extends sfActions 698 { 699 public function executeIndex() 700 { 701 // Some code there 702 } 703 } 704 705 // In myPlugin/modules/mymodule/actions/actions.class.php 706 class mymoduleActions extends myPluginmymoduleActions 707 { 708 // Nothing 709 } 710 711 // In myapp/modules/mymodule/actions/actions.class.php 712 class mymoduleActions extends myPluginmymoduleActions 713 { 714 public function executeIndex() 715 { 716 // Override the plug-in code there 717 } 718 } 719 720 ### How to Write a Plug-In 721 722 Only plug-ins packaged as PEAR packages can be installed with the `plugin-install` task. Remember that such plug-ins can be distributed via the symfony wiki, a PEAR channel, or a simple file download. So if you want to author a plug-in, it is better to publish it as a PEAR package than as a simple archive. In addition, PEAR packaged plug-ins are easier to upgrade, can declare dependencies, and automatically deploy assets in the `web/` directory. 723 724 #### File Organization 725 726 Suppose you have developed a new feature and want to package it as a plug-in. The first step is to organize the files logically so that the symfony loading mechanisms can find them when needed. For that purpose, you have to follow the structure given in Listing 17-23. Listing 17-27 shows an example of file structure for an `sfSamplePlugin` plug-in. 727 728 Listing 17-27 - Example List of Files to Package As a Plug-In 729 730 sfSamplePlugin/ 731 README 732 LICENSE 733 config/ 734 schema.yml 735 data/ 736 fixtures/ 737 fixtures.yml 738 tasks/ 739 sfSampleTask.php 740 lib/ 741 model/ 742 sfSampleFooBar.php 743 sfSampleFooBarPeer.php 744 validator/ 745 sfSampleValidator.class.php 746 modules/ 747 sfSampleModule/ 748 actions/ 749 actions.class.php 750 config/ 751 security.yml 752 lib/ 753 BasesfSampleModuleActions.class.php 754 templates/ 755 indexSuccess.php 756 web/ 757 css/ 758 sfSampleStyle.css 759 images/ 760 sfSampleImage.png 761 762 For authoring, the location of the plug-in directory (`sfSamplePlugin/` in Listing 17-27) is not important. It can be anywhere on the disk. 763 764 >**TIP** 765 >Take examples of the existing plug-ins and, for your first attempts at creating a plug-in, try to reproduce their naming conventions and file structure. 766 767 #### Creating the package.xml File 768 769 The next step of plug-in authoring is to add a package.xml file at the root of the plug-in directory. The `package.xml` follows the PEAR syntax. Have a look at a typical symfony plug-in `package.xml` in Listing 17-28. 770 771 Listing 17-28 - Example `package.xml` for a Symfony Plug-In 772 773 [xml] 774 <?xml version="1.0" encoding="UTF-8"?> 775 <package packagerversion="1.4.6" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> 776 <name>sfSamplePlugin</name> 777 <channel>pear.symfony-project.com</channel> 778 <summary>symfony sample plugin</summary> 779 <description>Just a sample plugin to illustrate PEAR packaging</description> 780 <lead> 781 <name>Fabien POTENCIER</name> 782 <user>fabpot</user> 783 <email>fabien.potencier@symfony-project.com</email> 784 <active>yes</active> 785 </lead> 786 <date>2006-01-18</date> 787 <time>15:54:35</time> 788 <version> 789 <release>1.0.0</release> 790 <api>1.0.0</api> 791 </version> 792 <stability> 793 <release>stable</release> 794 <api>stable</api> 795 </stability> 796 <license uri="http://www.symfony-project.com/license">MIT license</license> 797 <notes>-</notes> 798 <contents> 799 <dir name="/"> 800 <file role="data" name="README" /> 801 <file role="data" name="LICENSE" /> 802 <dir name="config"> 803 <!-- model --> 804 <file role="data" name="schema.yml" /> 805 </dir> 806 <dir name="data"> 807 <dir name="fixtures"> 808 <!-- fixtures --> 809 <file role="data" name="fixtures.yml" /> 810 </dir> 811 <dir name="tasks"> 812 <!-- tasks --> 813 <file role="data" name="sfSampleTask.php" /> 814 </dir> 815 </dir> 816 <dir name="lib"> 817 <dir name="model"> 818 <!-- model classes --> 819 <file role="data" name="sfSampleFooBar.php" /> 820 <file role="data" name="sfSampleFooBarPeer.php" /> 821 </dir> 822 <dir name="validator"> 823 <!-- validators ->> 824 <file role="data" name="sfSampleValidator.class.php" /> 825 </dir> 826 </dir> 827 <dir name="modules"> 828 <dir name="sfSampleModule"> 829 <file role="data" name="actions/actions.class.php" /> 830 <file role="data" name="config/security.yml" /> 831 <file role="data" name="lib/BasesfSampleModuleActions.class.php" /> 832 <file role="data" name="templates/indexSuccess.php" /> 833 </dir> 834 </dir> 835 <dir name="web"> 836 <dir name="css"> 837 <!-- stylesheets --> 838 <file role="data" name="sfSampleStyle.css" /> 839 </dir> 840 <dir name="images"> 841 <!-- images --> 842 <file role="data" name="sfSampleImage.png" /> 843 </dir> 844 </dir> 845 </dir> 846 </contents> 847 <dependencies> 848 <required> 849 <php> 850 <min>5.0.0</min> 851 </php> 852 <pearinstaller> 853 <min>1.4.1</min> 854 </pearinstaller> 855 <package> 856 <name>symfony</name> 857 <channel>pear.symfony-project.com</channel> 858 <min>1.0.0</min> 859 <max>1.1.0</max> 860 <exclude>1.1.0</exclude> 861 </package> 862 </required> 863 </dependencies> 864 <phprelease /> 865 <changelog /> 866 </package> 867 868 The interesting parts here are the `<contents>` and the `<dependencies>` tags, described next. For the rest of the tags, there is nothing specific to symfony, so you can refer to the PEAR online manual ([http://pear.php.net/manual/en/](http://pear.php.net/manual/en/)) for more details about the `package.xml` format. 869 870 #### Contents 871 872 The `<contents>` tag is the place where you must describe the plug-in file structure. This will tell PEAR which files to copy and where. Describe the file structure with `<dir>` and `<file>` tags. All `<file>` tags must have a `role="data"` attribute. The `<contents>` part of Listing 17-28 describes the exact directory structure of Listing 17-27. 873 874 >**NOTE** 875 >The use of `<dir>` tags is not compulsory, since you can use relative paths as `name` values in the `<file>` tags. However, it is recommended so that the `package.xml` file remains readable. 876 877 #### Plug-In Dependencies 878 879 Plug-ins are designed to work with a given set of versions of PHP, PEAR, symfony, PEAR packages, or other plug-ins. Declaring these dependencies in the `<dependencies>` tag tells PEAR to check that the required packages are already installed, and to raise an exception if not. 880 881 You should always declare dependencies on PHP, PEAR, and symfony, at least the ones corresponding to your own installation, as a minimum requirement. If you don't know what to put, add a requirement for PHP 5.0, PEAR 1.4, and symfony 1.0. 882 883 It is also recommended to add a maximum version number of symfony for each plug-in. This will cause an error message when trying to use a plug-in with a more advanced version of the framework, and this will oblige the plug-in author to make sure that the plug-in works correctly with this version before releasing it again. It is better to have an alert and to download an upgrade rather than have a plug-in fail silently. 884 885 #### Building the Plug-In 886 887 The PEAR component has a command (`pear package`) that creates the `.tgz` archive of the package, provided you call the command shown in Listing 17-29 from a directory containing a `package.xml`. 888 889 Listing 17-29 - Packaging a Plug-In As a PEAR Package 890 891 > cd sfSamplePlugin 892 > pear package 893 894 Package sfSamplePlugin-1.0.0.tgz done 895 896 Once your plug-in is built, check that it works by installing it yourself, as shown in Listing 17-30. 897 898 Listing 17-30 - Installing the Plug-In 899 900 > cp sfSamplePlugin-1.0.0.tgz /home/production/myproject/ 901 > cd /home/production/myproject/ 902 > php symfony plugin-install sfSamplePlugin-1.0.0.tgz 903 904 According to their description in the `<contents>` tag, the packaged files will end up in different directories of your project. Listing 17-31 shows where the files of the `sfSamplePlugin` should end up after installation. 905 906 Listing 17-31 - The Plug-In Files Are Installed on the `plugins/` and `web/` Directories 907 908 plugins/ 909 sfSamplePlugin/ 910 README 911 LICENSE 912 config/ 913 schema.yml 914 data/ 915 fixtures/ 916 fixtures.yml 917 tasks/ 918 sfSampleTask.php 919 lib/ 920 model/ 921 sfSampleFooBar.php 922 sfSampleFooBarPeer.php 923 validator/ 924 sfSampleValidator.class.php 925 modules/ 926 sfSampleModule/ 927 actions/ 928 actions.class.php 929 config/ 930 security.yml 931 lib/ 932 BasesfSampleModuleActions.class.php 933 templates/ 934 indexSuccess.php 935 web/ 936 sfSamplePlugin/ ## Copy or symlink, depending on system 937 css/ 938 sfSampleStyle.css 939 images/ 940 sfSampleImage.png 941 942 Test the way the plug-in behaves in your application. If it works well, you are ready to distribute it across projects--or to contribute it to the symfony community. 943 944 #### Hosting Your Plug-In in the Symfony Project Website 945 946 A symfony plug-in gets the broadest audience when distributed by the `symfony-project.com` website. Even your own plug-ins can be distributed this way, provided that you follow these steps: 947 948 1. Make sure the `README` file describes the way to install and use your plug-in, and that the `LICENSE` file gives the license details. Format your `README` with the Wiki Formatting syntax ([http://www.symfony-project.com/trac/wiki/WikiFormatting](http://www.symfony-project.com/trac/wiki/WikiFormatting)). 949 2. Create a PEAR package for your plug-in by calling the `pear package` command, and test it. The PEAR package must be named `sfSamplePlugin-1.0.0.tgz` (1.0.0 is the plug-in version). 950 3. Create a new page on the symfony wiki named `sfSamplePlugin` (`Plugin` is a mandatory suffix). In this page, describe the plug-in usage, the license, the dependencies, and the installation procedure. You can reuse the contents of the plug-in `README` file. Check the existing plug-ins' wiki pages and use them as an example. 951 4. Attach your PEAR package to the wiki page (`sfSamplePlugin-1.0.0.tgz`). 952 5. Add the new plug-in wiki page (`[wiki:sfSamplePlugin]`) to the list of available plug-ins, which is also a wiki page ([http://www.symfony-project.com/trac/wiki/SymfonyPlugins](http://www.symfony-project.com/trac/wiki/SymfonyPlugins)). 953 954 If you follow this procedure, users will be able to install your plug-in by simply typing the following command in a project directory: 955 956 > php symfony plugin-install http://plugins.symfony-project.com/sfSamplePlugin 957 958 #### Naming Conventions 959 960 To keep the `plugins/` directory clean, ensure all the plug-in names are in camelCase and end with `Plugin` (for example, `shoppingCartPlugin`, `feedPlugin`, and so on). Before naming your plug-in, check that there is no existing plug-in with the same name. 961 962 >**NOTE** 963 >Plug-ins relying on Propel should contain `Propel` in the name. For instance, an authentication plug-in using the Propel data access objects should be called `sfPropelAuth`. 964 965 Plug-ins should always include a `LICENSE` file describing the conditions of use and the chosen license. You are also advised to add a `README` file to explain the version changes, purpose of the plug-in, its effect, installation and configuration instructions, etc. 966 967 Summary 968 ------- 969 970 The symfony classes contain `sfMixer` hooks that give them the ability to be modified at the application level. The mixins mechanism allows multiple inheritance and class overriding at runtime even if the PHP limitations forbid it. So you can easily extend the symfony features, even if you have to modify the core classes for that--the factories configuration is here for that. 971 972 Many such extensions already exist; they are packaged as plug-ins, to be easily installed, upgraded, and uninstalled through the symfony command line. Creating a plug-in is as easy as creating a PEAR package, and provides reusability across applications. 973 974 The symfony wiki contains many plug-ins, and you can even add your own. So now that you know how to do it, we hope that you will enhance the symfony core with a lot of useful extensions!
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
Généré le : Fri Mar 16 22:42:14 2007 | par Balluche grâce à PHPXref 0.7 |