17-Extending-Symfony.txt 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101
  1. Chapter 17 - Extending Symfony
  2. ==============================
  3. 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.
  4. Events
  5. ------
  6. 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)).
  7. ### Understanding Events
  8. 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".
  9. 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.
  10. Listing 17-1 - Registering an Event Listener
  11. [php]
  12. $dispatcher->connect('user.change_culture', 'changeUserCulture');
  13. function changeUserCulture(sfEvent $event)
  14. {
  15. $user = $event->getSubject();
  16. $culture = $event['culture'];
  17. // do something with the user culture
  18. }
  19. 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.
  20. 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.
  21. 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`).
  22. 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.
  23. >**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.
  24. ### Notifying an Event
  25. 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.
  26. Listing 17-2 - Notifying an Event
  27. [php]
  28. class sfRestRequest
  29. {
  30. protected $dispatcher = null;
  31. public function __construct(sfEventDispatcher $dispatcher)
  32. {
  33. $this->dispatcher = $dispatcher;
  34. }
  35. /**
  36. * Makes a query to an external web service
  37. */
  38. public function fetch($uri, $parameters = array())
  39. {
  40. // Notify the beginning of the fetch process
  41. $this->dispatcher->notify(new sfEvent($this, 'rest_request.fetch_prepare', array(
  42. 'uri' => $uri,
  43. 'parameters' => $parameters
  44. )));
  45. // Make the request and store the result in a $result variable
  46. // ...
  47. // Notify the end of the fetch process
  48. $this->dispatcher->notify(new sfEvent($this, 'rest_request.fetch_success', array(
  49. 'uri' => $uri,
  50. 'parameters' => $parameters,
  51. 'result' => $result
  52. )));
  53. return $result;
  54. }
  55. }
  56. 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.
  57. >**TIP**
  58. >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.
  59. ### Notifying an Event Until a Listener handles it
  60. 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.
  61. Listing 17-3 - Notifying an Event Until a Listener Returns True
  62. [php]
  63. class sfRestRequest
  64. {
  65. // ...
  66. public function __call($method, $arguments)
  67. {
  68. $event = $this->dispatcher->notifyUntil(new sfEvent($this, 'rest_request.method_not_found', array(
  69. 'method' => $method,
  70. 'arguments' => $arguments
  71. )));
  72. if (!$event->isProcessed())
  73. {
  74. throw new sfException(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
  75. }
  76. return $event->getReturnValue();
  77. }
  78. }
  79. 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.
  80. Listing 17-4 - Handling a "Notify Until"-Type Event
  81. [php]
  82. class frontendConfiguration extends sfApplicationConfiguration
  83. {
  84. public function configure()
  85. {
  86. // ...
  87. // Register our listener
  88. $this->dispatcher->connect('rest_request.method_not_found', array('sfRestRequestExtension', 'listenToMethodNotFound'));
  89. }
  90. }
  91. class sfRestRequestExtension
  92. {
  93. static public function listenToMethodNotFound(sfEvent $event)
  94. {
  95. switch ($event['method'])
  96. {
  97. case 'put':
  98. self::put($event->getSubject(), $event['arguments'])
  99. return true;
  100. case 'delete':
  101. self::delete($event->getSubject(), $event['arguments'])
  102. return true;
  103. default:
  104. return false;
  105. }
  106. }
  107. static protected function put($restRequest, $arguments)
  108. {
  109. // Make a put request and store the result in a $result variable
  110. // ...
  111. $event->setReturnValue($result);
  112. }
  113. static protected function delete($restRequest, $arguments)
  114. {
  115. // Make a delete request and store the result in a $result variable
  116. // ...
  117. $event->setReturnValue($result);
  118. }
  119. }
  120. 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.
  121. >**TIP**
  122. >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.
  123. ### Changing the Return Value of a Method
  124. 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.
  125. Listing 17-5 - Notifying and Handling a Filter Event
  126. [php]
  127. class sfRestRequest
  128. {
  129. // ...
  130. /**
  131. * Make a query to an external web service
  132. */
  133. public function fetch($uri, $parameters = array())
  134. {
  135. // Make the request and store the result in a $result variable
  136. // ...
  137. // Notify the end of the fetch process
  138. return $this->dispatcher->filter(new sfEvent($this, 'rest_request.filter_result', array(
  139. 'uri' => $uri,
  140. 'parameters' => $parameters,
  141. ), $result));
  142. }
  143. }
  144. // Add escaping to the web service response
  145. $dispatcher->connect('rest_request.filter_result', 'rest_htmlspecialchars');
  146. function rest_htmlspecialchars(sfEvent $event, $result)
  147. {
  148. return htmlspecialchars($result, ENT_QUOTES, 'UTF-8');
  149. }
  150. ### Built-In Events
  151. 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.
  152. Table 17-1 - Symfony's Events
  153. | **event namespace** | **event name** | **type** | **notifiers** | **arguments** |
  154. | ------------------- | ------------------ | ----------- | ---------------------- | --------------------------- |
  155. | **application** | log | notify | lot of classes | priority |
  156. | | throw_exception | notifyUntil | sfException | - |
  157. | **command** | log | notify | sfCommand* classes | priority |
  158. | | pre_command | notifyUntil | sfTask | arguments, options |
  159. | | post_command | notify | sfTask | - |
  160. | | filter_options | filter | sfTask | command_manager |
  161. | **configuration** | method_not_found | notifyUntil | sfProjectConfiguration | method, arguments |
  162. | **component** | method_not_found | notifyUntil | sfComponent | method, arguments |
  163. | **context** | load_factories | notify | sfContext | - |
  164. | **controller** | change_action | notify | sfController | module, action |
  165. | | method_not_found | notifyUntil | sfController | method, arguments |
  166. | | page_not_found | notify | sfController | module, action |
  167. | **plugin** | pre_install | notify | sfPluginManager | channel, plugin, is_package |
  168. | | post_install | notify | sfPluginManager | channel, plugin |
  169. | | pre_uninstall | notify | sfPluginManager | channel, plugin |
  170. | | post_uninstall | notify | sfPluginManager | channel, plugin |
  171. | **request** | filter_parameters | filter | sfWebRequest | path_info |
  172. | | method_not_found | notifyUntil | sfRequest | method, arguments |
  173. | **response** | method_not_found | notifyUntil | sfResponse | method, arguments |
  174. | | filter_content | filter | sfResponse | - |
  175. | **routing** | load_configuration | notify | sfRouting | - |
  176. | **task** | cache.clear | notifyUntil | sfCacheClearTask | app, type, env |
  177. | **template** | filter_parameters | filter | sfViewParameterHolder | - |
  178. | **user** | change_culture | notify | sfUser | culture |
  179. | | method_not_found | notifyUntil | sfUser | method, arguments |
  180. | **view** | configure_format | notify | sfView | format, response, request |
  181. | | method_not_found | notifyUntil | sfView | method, arguments |
  182. | **view.cache** | filter_content | filter | sfViewCacheManager | response, uri, new |
  183. 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.
  184. 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):
  185. [php]
  186. $dispatcher->notify(new sfEvent($this, 'application.log', array($message)));
  187. Your own classes can do the same and also notify symfony events when it makes sense.
  188. ### Where To Register Listeners?
  189. 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.
  190. Listing 17-6 - Registering a Listener in the Application Configuration Class, in `apps/frontend/config/ApplicationConfiguration.class.php`
  191. [php]
  192. class frontendConfiguration extends sfApplicationConfiguration
  193. {
  194. public function configure()
  195. {
  196. $this->dispatcher->connect('rest_request.method_not_found', array('sfRestRequestExtension', 'listenToMethodNotFound'));
  197. }
  198. }
  199. 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`.
  200. >**SIDEBAR**
  201. >Propel Behaviors
  202. >
  203. >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.
  204. >
  205. >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.
  206. >
  207. >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.
  208. >
  209. >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.
  210. Factories
  211. ---------
  212. 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.
  213. 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.
  214. Listing 17-7 - Default Factories File, in `frontend/config/factories.yml`
  215. prod:
  216. logger:
  217. class: sfNoLogger
  218. param:
  219. level: err
  220. loggers: ~
  221. cli:
  222. controller:
  223. class: sfConsoleController
  224. request:
  225. class: sfConsoleRequest
  226. response:
  227. class: sfConsoleResponse
  228. test:
  229. storage:
  230. class: sfSessionTestStorage
  231. param:
  232. session_path: %SF_TEST_CACHE_DIR%/sessions
  233. #all:
  234. # controller:
  235. # class: sfFrontWebController
  236. #
  237. # request:
  238. # class: sfWebRequest
  239. # param:
  240. # formats:
  241. # txt: text/plain
  242. # js: [application/javascript, application/x-javascript, text/javascript]
  243. # css: text/css
  244. # json: [application/json, application/x-json]
  245. # xml: [text/xml, application/xml, application/x-xml]
  246. # rdf: application/rdf+xml
  247. # atom: application/atom+xml
  248. #
  249. # response:
  250. # class: sfWebResponse
  251. # param:
  252. # logging: %SF_LOGGING_ENABLED%
  253. # charset: %SF_CHARSET%
  254. #
  255. # user:
  256. # class: myUser
  257. # param:
  258. # timeout: 1800
  259. # logging: %SF_LOGGING_ENABLED%
  260. # use_flash: true
  261. # default_culture: %SF_DEFAULT_CULTURE%
  262. #
  263. # storage:
  264. # class: sfSessionStorage
  265. # param:
  266. # session_name: symfony
  267. #
  268. # view_cache:
  269. # class: sfFileCache
  270. # param:
  271. # automatic_cleaning_factor: 0
  272. # cache_dir: %SF_TEMPLATE_CACHE_DIR%
  273. # lifetime: 86400
  274. # prefix: %SF_APP_DIR%
  275. #
  276. # i18n:
  277. # class: sfI18N
  278. # param:
  279. # source: XLIFF
  280. # debug: off
  281. # untranslated_prefix: "[T]"
  282. # untranslated_suffix: "[/T]"
  283. # cache:
  284. # class: sfFileCache
  285. # param:
  286. # automatic_cleaning_factor: 0
  287. # cache_dir: %SF_I18N_CACHE_DIR%
  288. # lifetime: 86400
  289. # prefix: %SF_APP_DIR%
  290. #
  291. # routing:
  292. # class: sfPatternRouting
  293. # param:
  294. # load_configuration: true
  295. # suffix: .
  296. # default_module: default
  297. # default_action: index
  298. # variable_prefixes: [':']
  299. # segment_separators: ['/', '.']
  300. # variable_regex: '[\w\d_]+'
  301. # debug: %SF_DEBUG%
  302. # logging: %SF_LOGGING_ENABLED%
  303. # cache:
  304. # class: sfFileCache
  305. # param:
  306. # automatic_cleaning_factor: 0
  307. # cache_dir: %SF_CONFIG_CACHE_DIR%/routing
  308. # lifetime: 31556926
  309. # prefix: %SF_APP_DIR%
  310. #
  311. # logger:
  312. # class: sfAggregateLogger
  313. # param:
  314. # level: debug
  315. # loggers:
  316. # sf_web_debug:
  317. # class: sfWebDebugLogger
  318. # param:
  319. # condition: %SF_WEB_DEBUG%
  320. # xdebug_logging: true
  321. # sf_file_debug:
  322. # class: sfFileLogger
  323. # param:
  324. # file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%.log
  325. 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.
  326. Listing 17-8 - Overriding Factories
  327. [php]
  328. // Create a myRequest.class.php in an autoloaded directory,
  329. // For instance in frontend/lib/
  330. <?php
  331. class myRequest extends sfRequest
  332. {
  333. // Your code here
  334. }
  335. // Declare this class as the request factory in factories.yml
  336. all:
  337. request:
  338. class: myRequest
  339. Integrating with Other Framework's Components
  340. ---------------------------------------------
  341. 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.
  342. 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.
  343. 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`:
  344. all:
  345. zend_lib_dir: /usr/local/zend/library/
  346. ez_lib_dir: /usr/local/ezcomponents/
  347. swift_lib_dir: /usr/local/swiftmailer/
  348. 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.
  349. Listing 17-9 - Extending the Autoloading System To Enable Third Party Components, in apps/frontend/config/ApplicationConfiguration.class.php
  350. [php]
  351. class frontendConfiguration extends sfApplicationConfiguration
  352. {
  353. public function initialize()
  354. {
  355. parent::initialize(); // load symfony autoloading first
  356. // Integrate Zend Framework
  357. if ($sf_zend_lib_dir = sfConfig::get('app_zend_lib_dir'))
  358. {
  359. set_include_path($sf_zend_lib_dir.PATH_SEPARATOR.get_include_path());
  360. require_once($sf_zend_lib_dir.'/Zend/Loader.php');
  361. spl_autoload_register(array('Zend_Loader', 'autoload'));
  362. }
  363. // Integrate eZ Components
  364. if ($sf_ez_lib_dir = sfConfig::get('app_ez_lib_dir'))
  365. {
  366. set_include_path($sf_ez_lib_dir.PATH_SEPARATOR.get_include_path());
  367. require_once($sf_ez_lib_dir.'/Base/base.php');
  368. spl_autoload_register(array('ezcBase', 'autoload'));
  369. }
  370. // Integrate Swift Mailer
  371. if ($sf_swift_lib_dir = sfConfig::get('app_swift_lib_dir'))
  372. {
  373. set_include_path($sf_swift_lib_dir.PATH_SEPARATOR.get_include_path());
  374. require_once($sf_swift_lib_dir.'/Swift/ClassLoader.php');
  375. spl_autoload_register(array('Swift_ClassLoader', 'load'));
  376. }
  377. }
  378. }
  379. What happens when you create a new object of an unloaded class is simple:
  380. 1. The symfony autoloading function first looks for a class in the paths declared in the `autoload.yml` file.
  381. 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.
  382. 3. If these also return `false`, PHP will generate an error.
  383. 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:
  384. [php]
  385. require_once 'Zend/Search/Lucene.php';
  386. $doc = new Zend_Search_Lucene_Document();
  387. $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
  388. // ...
  389. With symfony and spl autoloading, it is simpler. You can omit the `require` statement and stop worrying about include paths and class locations:
  390. [php]
  391. $doc = new Zend_Search_Lucene_Document(); // The class is autoloaded
  392. $doc->addField(Zend_Search_Lucene_Field::Text('url', $docUrl));
  393. // ...
  394. Plug-Ins
  395. --------
  396. 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.
  397. 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.
  398. 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.
  399. ### Finding Symfony Plug-Ins
  400. The symfony project website contains a page dedicated to symfony plug-ins. It is in the symfony wiki and accessible with the following URL:
  401. http://www.symfony-project.org/plugins/
  402. Each plug-in listed there has its own page, with detailed installation instructions and documentation.
  403. 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:
  404. * `sfFeed2Plugin`: Automates the manipulation of RSS and Atom feeds
  405. * `sfThumbnailPlugin`: Creates thumbnails--for instance, for uploaded images
  406. * `sfMediaLibraryPlugin`: Allows media upload and management, including an extension for rich text editors to allow authoring of images inside rich text
  407. * `sfShoppingCartPlugin`: Allows shopping cart management
  408. * `sfPagerNavigationPlugin`: Provides classical and Ajax pager controls, based on an `sfPager` object
  409. * `sfGuardPlugin`: Provides authentication, authorization, and other user management features above the standard security feature of symfony
  410. * `sfPrototypePlugin`: Provides prototype and script.aculo.us JavaScript files as a standalone library
  411. * `sfSuperCachePlugin`: Writes pages in cache directory under the web root to allow the web server to serve them as fast as possible
  412. * `sfOptimizerPlugin`: Optimizes your application's code to make it execute faster in the production environment (see the next chapter for details)
  413. * `sfErrorLoggerPlugin`: Logs every 404 and 500 error in a database and provides an administration module to browse these errors
  414. * `sfSslRequirementPlugin`: Provides SSL encryption support for actions
  415. The wiki also proposes plug-ins designed to extend your Propel objects, also called behaviors. Among them, you will find the following:
  416. * `sfPropelParanoidBehaviorPlugin`: Disables object deletion and replaces it with the updating of a `deleted_at` column
  417. * `sfPropelOptimisticLockBehaviorPlugin`: Implements optimistic locking for Propel objects
  418. 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.
  419. 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.
  420. ### Installing a Plug-In
  421. 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.
  422. 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.
  423. #### PEAR Plug-Ins
  424. 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.
  425. Listing 17-10 - Installing a Plug-In from the Official symfony plugins PEAR Channel / Symfony Wiki
  426. > cd myproject
  427. > php symfony plugin:install pluginName
  428. 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.
  429. Listing 17-11 - Installing a Plug-In from a Downloaded PEAR Package
  430. > cd myproject
  431. > php symfony plugin:install /home/path/to/downloads/pluginName.tgz
  432. 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.
  433. Listing 17-12 - Installing a Plug-In from a PEAR Channel
  434. > cd myproject
  435. > php symfony plugin:add-channel channel.symfony.pear.example.com
  436. > php symfony plugin:install --channel=channel.symfony.pear.example.com pluginName
  437. 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.
  438. The `plugin:install` task also takes a number of options, as shown on Listing 17-13.
  439. Listing 17-13 - Installing a Plug-In with some Options
  440. > php symfony plugin:install --stability=beta pluginName
  441. > php symfony plugin:install --release=1.0.3 pluginName
  442. > php symfony plugin:install --install-deps pluginName
  443. >**TIP**
  444. >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`.
  445. #### Archive Plug-Ins
  446. 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.
  447. Listing 17-14 - Installing a Plug-In from an Archive
  448. > cd plugins
  449. > tar -zxpf myPlugin.tgz
  450. > cd ..
  451. > ln -sf plugins/myPlugin/web web/myPlugin
  452. > php symfony cc
  453. #### Installing Plug-Ins from a Version Control Repository
  454. 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.
  455. 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.
  456. Listing 17-15 - Installing a Plug-In from a Source Version Repository
  457. > cd myproject
  458. > svn propedit svn:externals plugins
  459. pluginName http://svn.example.com/pluginName/trunk
  460. > svn up
  461. > php symfony cc
  462. >**NOTE**
  463. >If the plug-in contains a `web/` directory, you must create a symlink to it the same way as for an archive plug-in.
  464. #### Activating a Plug-In Module
  465. 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.
  466. Listing 17-16 - Activating a Plug-In Module, in `frontend/config/settings.yml`
  467. all:
  468. .settings:
  469. enabled_modules: [default, sfMyPluginModule]
  470. 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.
  471. >**TIP**
  472. >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.
  473. #### Listing the Installed Plug-Ins
  474. 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).
  475. Listing 17-17 - Listing Installed Plug-Ins
  476. > cd myproject
  477. > php symfony plugin:list
  478. Installed plugins:
  479. sfPrototypePlugin 1.0.0-stable # plugins.symfony-project.com (symfony)
  480. sfSuperCachePlugin 1.0.0-stable # plugins.symfony-project.com (symfony)
  481. sfThumbnail 1.1.0-stable # plugins.symfony-project.com (symfony)
  482. #### Upgrading and Uninstalling Plug-Ins
  483. 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).
  484. Listing 17-18 - Uninstalling a Plug-In
  485. > cd myproject
  486. > php symfony plugin:uninstall sfPrototypePlugin
  487. > php symfony cc
  488. 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.
  489. 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.
  490. ### Anatomy of a Plug-In
  491. 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.
  492. #### Plug-In File Structure
  493. 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.
  494. Listing 17-19 - File Structure of a Plug-In
  495. pluginName/
  496. config/
  497. *schema.yml // Data schema
  498. *schema.xml
  499. config.php // Specific plug-in configuration
  500. data/
  501. generator/
  502. sfPropelAdmin
  503. */ // Administration generator themes
  504. template/
  505. skeleton/
  506. fixtures/
  507. *.yml // Fixtures files
  508. lib/
  509. *.php // Classes
  510. helper/
  511. *.php // Helpers
  512. model/
  513. *.php // Model classes
  514. task/
  515. *Task.class.php // CLI tasks
  516. modules/
  517. */ // Modules
  518. actions/
  519. actions.class.php
  520. config/
  521. module.yml
  522. view.yml
  523. security.yml
  524. templates/
  525. *.php
  526. validate/
  527. *.yml
  528. web/
  529. * // Assets
  530. #### Plug-In Abilities
  531. 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:
  532. * 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.
  533. Listing 17-20 - Example Schema Declaration in a Plug-In, in `myPlugin/config/schema.yml`
  534. propel:
  535. _attributes: { package: plugins.myPlugin.lib.model }
  536. my_plugin_foobar:
  537. _attributes: { phpName: myPluginFoobar }
  538. id:
  539. name: { type: varchar, size: 255, index: unique }
  540. ...
  541. * 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.
  542. * Fixtures files located in the plug-in `data/fixtures/` directory are processed by the `propel:data-load` task.
  543. * Custom classes are autoloaded just like the ones you put in your project `lib/` folders.
  544. * 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.
  545. * 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.
  546. * 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.
  547. * Modules provide new actions accessible from the outside, provided that you declare them in the `enabled_modules` setting in your application.
  548. * 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).
  549. >**TIP**: Registering routing rules in a Plug-in
  550. >A plug-in can add new rules to the routing system, but it cannot use a custom `routing.yml` configuration file for that. 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:
  551. >
  552. > [php]
  553. > // in plugins/myPlugin/config/config.php
  554. > $this->dispatcher->connect('routing.load_configuration', array('myPluginRouting', 'listenToRoutingLoadConfigurationEvent'));
  555. >
  556. > // in plugins/myPlugin/lib/myPluginRouting.php
  557. > class myPluginRouting
  558. > {
  559. > static public function listenToRoutingLoadConfigurationEvent(sfEvent $event)
  560. > {
  561. > $routing = $event->getSubject();
  562. > // add plug-in routing rules on top of the existing ones
  563. > $routing->prependRoute('my_route', '/my_plugin/:action', array('module' => 'myPluginAdministrationInterface'));
  564. > }
  565. > }
  566. >
  567. #### Manual Plug-In Setup
  568. There are some elements that the `plugin:install` task cannot handle on its own, and which require manual setup during installation:
  569. * 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).
  570. * Custom routing rules have to be added manually to the application `routing.yml`.
  571. * Custom filters have to be added manually to the application `filters.yml`.
  572. * Custom factories have to be added manually to the application `factories.yml`.
  573. 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.
  574. #### Customizing a Plug-In for an Application
  575. 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.
  576. Well-designed plug-ins use settings that can be changed in the application `app.yml`, as Listing 17-21 demonstrates.
  577. Listing 17-21 - Customizing a Plug-In That Uses the Application Configuration
  578. [php]
  579. // example plug-in code
  580. $foo = sfConfig::get('app_my_plugin_foo', 'bar');
  581. // Change the 'foo' default value ('bar') in the application app.yml
  582. all:
  583. my_plugin:
  584. foo: barbar
  585. The module settings and their default values are often described in the plug-in's `README` file.
  586. 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.
  587. 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.
  588. Listing 17-22 - Customizing a Plug-In Action
  589. [php]
  590. // In myPlugin/modules/mymodule/lib/myPluginmymoduleActions.class.php
  591. class myPluginmymoduleActions extends sfActions
  592. {
  593. public function executeIndex()
  594. {
  595. // Some code there
  596. }
  597. }
  598. // In myPlugin/modules/mymodule/actions/actions.class.php
  599. require_once dirname(__FILE__).'/../lib/myPluginmymoduleActions.class.php';
  600. class mymoduleActions extends myPluginmymoduleActions
  601. {
  602. // Nothing
  603. }
  604. // In frontend/modules/mymodule/actions/actions.class.php
  605. class mymoduleActions extends myPluginmymoduleActions
  606. {
  607. public function executeIndex()
  608. {
  609. // Override the plug-in code there
  610. }
  611. }
  612. >**SIDEBAR**
  613. >**New in symfony 1.1**: Customizing the plug-in schema
  614. >
  615. >When building the model, symfony will look for custom YAML files for each existing schema, including plug-in ones, following this rule:
  616. >
  617. >Original schema name | Custom schema name
  618. >-------------------------------------- | ------------------------------
  619. >config/schema.yml | schema.custom.yml
  620. >config/foobar_schema.yml | foobar_schema.custom.yml
  621. >plugins/myPlugin/config/schema.yml | myPlugin_schema.custom.yml
  622. >plugins/myPlugin/config/foo_schema.yml | myPlugin_foo_schema.custom.yml
  623. >
  624. >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.
  625. >
  626. >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.
  627. >
  628. > # Original schema, in plugins/myPlugin/config/schema.yml
  629. > propel:
  630. > article:
  631. > _attributes: { phpName: Article }
  632. > title: varchar(50)
  633. > user_id: { type: integer }
  634. > created_at:
  635. >
  636. > # Custom schema, in myPlugin_schema.custom.yml
  637. > propel:
  638. > article:
  639. > _attributes: { phpName: Article, package: foo.bar.lib.model }
  640. > stripped_title: varchar(50)
  641. >
  642. > # Resulting schema, merged internally and used for model and sql generation
  643. > propel:
  644. > article:
  645. > _attributes: { phpName: Article, package: foo.bar.lib.model }
  646. > title: varchar(50)
  647. > user_id: { type: integer }
  648. > created_at:
  649. > stripped_title: varchar(50)
  650. >
  651. >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.
  652. ### How to Write a Plug-In
  653. 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.
  654. #### File Organization
  655. 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.
  656. Listing 17-23 - Example List of Files to Package As a Plug-In
  657. sfSamplePlugin/
  658. README
  659. LICENSE
  660. config/
  661. schema.yml
  662. data/
  663. fixtures/
  664. fixtures.yml
  665. lib/
  666. model/
  667. sfSampleFooBar.php
  668. sfSampleFooBarPeer.php
  669. task/
  670. sfSampleTask.class.php
  671. validator/
  672. sfSampleValidator.class.php
  673. modules/
  674. sfSampleModule/
  675. actions/
  676. actions.class.php
  677. config/
  678. security.yml
  679. lib/
  680. BasesfSampleModuleActions.class.php
  681. templates/
  682. indexSuccess.php
  683. web/
  684. css/
  685. sfSampleStyle.css
  686. images/
  687. sfSampleImage.png
  688. For authoring, the location of the plug-in directory (`sfSamplePlugin/` in Listing 17-23) is not important. It can be anywhere on the disk.
  689. >**TIP**
  690. >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.
  691. #### Creating the package.xml File
  692. 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.
  693. Listing 17-24 - Example `package.xml` for a Symfony Plug-In
  694. [xml]
  695. <?xml version="1.0" encoding="UTF-8"?>
  696. <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">
  697. <name>sfSamplePlugin</name>
  698. <channel>plugins.symfony-project.org</channel>
  699. <summary>symfony sample plugin</summary>
  700. <description>Just a sample plugin to illustrate PEAR packaging</description>
  701. <lead>
  702. <name>Fabien POTENCIER</name>
  703. <user>fabpot</user>
  704. <email>fabien.potencier@symfony-project.com</email>
  705. <active>yes</active>
  706. </lead>
  707. <date>2006-01-18</date>
  708. <time>15:54:35</time>
  709. <version>
  710. <release>1.0.0</release>
  711. <api>1.0.0</api>
  712. </version>
  713. <stability>
  714. <release>stable</release>
  715. <api>stable</api>
  716. </stability>
  717. <license uri="http://www.symfony-project.org/license">MIT license</license>
  718. <notes>-</notes>
  719. <contents>
  720. <dir name="/">
  721. <file role="data" name="README" />
  722. <file role="data" name="LICENSE" />
  723. <dir name="config">
  724. <!-- model -->
  725. <file role="data" name="schema.yml" />
  726. </dir>
  727. <dir name="data">
  728. <dir name="fixtures">
  729. <!-- fixtures -->
  730. <file role="data" name="fixtures.yml" />
  731. </dir>
  732. </dir>
  733. <dir name="lib">
  734. <dir name="model">
  735. <!-- model classes -->
  736. <file role="data" name="sfSampleFooBar.php" />
  737. <file role="data" name="sfSampleFooBarPeer.php" />
  738. </dir>
  739. <dir name="task">
  740. <!-- tasks -->
  741. <file role="data" name="sfSampleTask.class.php" />
  742. </dir>
  743. <dir name="validator">
  744. <!-- validators -->
  745. <file role="data" name="sfSampleValidator.class.php" />
  746. </dir>
  747. </dir>
  748. <dir name="modules">
  749. <dir name="sfSampleModule">
  750. <file role="data" name="actions/actions.class.php" />
  751. <file role="data" name="config/security.yml" />
  752. <file role="data" name="lib/BasesfSampleModuleActions.class.php" />
  753. <file role="data" name="templates/indexSuccess.php" />
  754. </dir>
  755. </dir>
  756. <dir name="web">
  757. <dir name="css">
  758. <!-- stylesheets -->
  759. <file role="data" name="sfSampleStyle.css" />
  760. </dir>
  761. <dir name="images">
  762. <!-- images -->
  763. <file role="data" name="sfSampleImage.png" />
  764. </dir>
  765. </dir>
  766. </dir>
  767. </contents>
  768. <dependencies>
  769. <required>
  770. <php>
  771. <min>5.1.0</min>
  772. </php>
  773. <pearinstaller>
  774. <min>1.4.1</min>
  775. </pearinstaller>
  776. <package>
  777. <name>symfony</name>
  778. <channel>pear.symfony-project.com</channel>
  779. <min>1.1.0</min>
  780. <max>1.2.0</max>
  781. <exclude>1.2.0</exclude>
  782. </package>
  783. </required>
  784. </dependencies>
  785. <phprelease />
  786. <changelog />
  787. </package>
  788. 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.
  789. #### Contents
  790. 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.
  791. >**NOTE**
  792. >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.
  793. #### Plug-In Dependencies
  794. 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.
  795. 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.
  796. 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.
  797. If you specify plugins as dependencies, users will be able to install your plugin and all its dependencies with a single command:
  798. > php symfony plugin:install --install-deps sfSamplePlugin
  799. #### Building the Plug-In
  800. 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`.
  801. Listing 17-25 - Packaging a Plug-In As a PEAR Package
  802. > cd sfSamplePlugin
  803. > pear package
  804. Package sfSamplePlugin-1.0.0.tgz done
  805. Once your plug-in is built, check that it works by installing it yourself, as shown in Listing 17-26.
  806. Listing 17-26 - Installing the Plug-In
  807. > cp sfSamplePlugin-1.0.0.tgz /home/production/myproject/
  808. > cd /home/production/myproject/
  809. > php symfony plugin:install sfSamplePlugin-1.0.0.tgz
  810. 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.
  811. Listing 17-27 - The Plug-In Files Are Installed on the `plugins/` and `web/` Directories
  812. plugins/
  813. sfSamplePlugin/
  814. README
  815. LICENSE
  816. config/
  817. schema.yml
  818. data/
  819. fixtures/
  820. fixtures.yml
  821. lib/
  822. model/
  823. sfSampleFooBar.php
  824. sfSampleFooBarPeer.php
  825. task/
  826. sfSampleTask.class.php
  827. validator/
  828. sfSampleValidator.class.php
  829. modules/
  830. sfSampleModule/
  831. actions/
  832. actions.class.php
  833. config/
  834. security.yml
  835. lib/
  836. BasesfSampleModuleActions.class.php
  837. templates/
  838. indexSuccess.php
  839. web/
  840. sfSamplePlugin/ ## Copy or symlink, depending on system
  841. css/
  842. sfSampleStyle.css
  843. images/
  844. sfSampleImage.png
  845. 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.
  846. #### Hosting Your Plug-In in the Symfony Project Website
  847. 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:
  848. 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)).
  849. 2. Create a symfony account (http://www.symfony-project.org/user/new) and create the plugin (http://www.symfony-project.org/plugins/new).
  850. 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).
  851. 4. Upload your PEAR package (`sfSamplePlugin-1.0.0.tgz`).
  852. 5. Your plugin must now appear in the list of plugins ([http://www.symfony-project.org/plugins/](http://www.symfony-project.org/plugins/)).
  853. If you follow this procedure, users will be able to install your plug-in by simply typing the following command in a project directory:
  854. > php symfony plugin:install sfSamplePlugin
  855. #### Naming Conventions
  856. 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.
  857. >**NOTE**
  858. >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`.
  859. 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.
  860. Summary
  861. -------
  862. 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.
  863. 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.
  864. 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!