08-Inside-the-Model-Layer.txt 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. Chapter 8 - Inside The Model Layer
  2. ==================================
  3. Much of the discussion so far has been devoted to building pages, and processing requests and responses. But the business logic of a web application relies mostly on its data model. Symfony's default model component is based on an object/relational mapping layer known as the Propel project ([http://propel.phpdb.org/](http://propel.phpdb.org/)). In a symfony application, you access data stored in a database and modify it through objects; you never address the database explicitly. This maintains a high level of abstraction and portability.
  4. This chapter explains how to create an object data model, and the way to access and modify the data in Propel. It also demonstrates the integration of Propel in Symfony.
  5. Why Use an ORM and an Abstraction Layer?
  6. ----------------------------------------
  7. Databases are relational. PHP 5 and symfony are object-oriented. In order to most effectively access the database in an object-oriented context, an interface translating the object logic to the relational logic is required. As explained in Chapter 1, this interface is called an object-relational mapping (ORM), and it is made up of objects that give access to data and keep business rules within themselves.
  8. The main benefit of an ORM is reusability, allowing the methods of a data object to be called from various parts of the application, even from different applications. The ORM layer also encapsulates the data logic--for instance, the calculation of a forum user rating based on how many contributions were made and how popular these contributions are. When a page needs to display such a user rating, it simply calls a method of the data model, without worrying about the details of the calculation. If the calculation changes afterwards, you will just need to modify the rating method in the model, leaving the rest of the application unchanged.
  9. Using objects instead of records, and classes instead of tables, has another benefit: They allow you to add new accessors to your objects that don't necessarily match a column in a table. For instance, if you have a table called `client` with two fields named `first_name` and `last_name`, you might like to be able to require just a `Name`. In an object-oriented world, it is as easy as adding a new accessor method to the `Client` class, as in Listing 8-1. From the application point of view, there is no difference between the `FirstName`, `LastName`, and `Name` attributes of the `Client` class. Only the class itself can determine which attributes correspond to a database column.
  10. Listing 8-1 - Accessors Mask the Actual Table Structure in a Model Class
  11. [php]
  12. public function getName()
  13. {
  14. return $this->getFirstName().' '.$this->getLastName();
  15. }
  16. All the repeated data-access functions and the business logic of the data itself can be kept in such objects. Suppose you have a `ShoppingCart` class in which you keep `Items` (which are objects). To get the full amount of the shopping cart for the checkout, write a custom method to encapsulate the actual calculation, as shown in Listing 8-2.
  17. Listing 8-2 - Accessors Mask the Data Logic
  18. [php]
  19. public function getTotal()
  20. {
  21. $total = 0;
  22. foreach ($this->getItems() as $item)
  23. {
  24. $total += $item->getPrice() * $item->getQuantity();
  25. }
  26. return $total;
  27. }
  28. There is another important point to consider when building data-access procedures: Database vendors use different SQL syntax variants. Switching to another database management system (DBMS) forces you to rewrite part of the SQL queries that were designed for the previous one. If you build your queries using a database-independent syntax, and leave the actual SQL translation to a third-party component, you can switch database systems without pain. This is the goal of the database abstraction layer. It forces you to use a specific syntax for queries, and does the dirty job of conforming to the DBMS particulars and optimizing the SQL code.
  29. The main benefit of an abstraction layer is portability, because it makes switching to another database possible, even in the middle of a project. Suppose that you need to write a quick prototype for an application, but the client hasn't decided yet which database system would best suit his needs. You can start building your application with SQLite, for instance, and switch to MySQL, PostgreSQL, or Oracle when the client is ready to decide. Just change one line in a configuration file, and it works.
  30. Symfony uses Propel as the ORM, and Propel uses Creole for database abstraction. These two third-party components, both developed by the Propel team, are seamlessly integrated into symfony, and you can consider them as part of the framework. Their syntax and conventions, described in this chapter, were adapted so that they differ from the symfony ones as little as possible.
  31. >**NOTE**
  32. >In a symfony project, all the applications share the same model. That's the whole point of the project level: regrouping applications that rely on common business rules. This is the reason that the model is independent from the applications and the model files are stored in a `lib/model/` directory at the root of the project.
  33. Symfony's Database Schema
  34. -------------------------
  35. In order to create the data object model that symfony will use, you need to translate whatever relational model your database has to an object data model. The ORM needs a description of the relational model to do the mapping, and this is called a schema. In a schema, you define the tables, their relations, and the characteristics of their columns.
  36. Symfony's syntax for schemas uses the YAML format. The `schema.yml` files must be located in the `myproject/config/` directory.
  37. >**NOTE**
  38. >Symfony also understands the Propel native XML schema format, as described in the "Beyond the schema.yml: The schema.xml" section later in this chapter.
  39. ### Schema Example
  40. How do you translate a database structure into a schema? An example is the best way to understand it. Imagine that you have a blog database with two tables: `blog_article` and `blog_comment`, with the structure shown in Figure 8-1.
  41. Figure 8-1 - A blog database table structure
  42. ![A blog database table structure](/images/book/F0801.png "A blog database table structure")
  43. The related `schema.yml` file should look like Listing 8-3.
  44. Listing 8-3 - Sample `schema.yml`
  45. [yml]
  46. propel:
  47. blog_article:
  48. _attributes: { phpName: Article }
  49. id:
  50. title: varchar(255)
  51. content: longvarchar
  52. created_at:
  53. blog_comment:
  54. _attributes: { phpName: Comment }
  55. id:
  56. article_id:
  57. author: varchar(255)
  58. content: longvarchar
  59. created_at:
  60. Notice that the name of the database itself (`blog`) doesn't appear in the `schema.yml` file. Instead, the database is described under a connection name (`propel` in this example). This is because the actual connection settings can depend on the environment in which your application runs. For instance, when you run your application in the development environment, you will access a development database (maybe `blog_dev`), but with the same schema as the production database. The connection settings will be specified in the `databases.yml` file, described in the "Database Connections" section later in this chapter. The schema doesn't contain any detailed connection to settings, only a connection name, to maintain database abstraction.
  61. ### Basic Schema Syntax
  62. In a `schema.yml` file, the first key represents a connection name. It can contain several tables, each having a set of columns. According to the YAML syntax, the keys end with a colon, and the structure is shown through indentation (one or more spaces, but no tabulations).
  63. A table can have special attributes, including the `phpName` (the name of the class that will be generated). If you don't mention a `phpName` for a table, symfony creates it based on the camelCase version of the table name.
  64. >**TIP**
  65. >The camelCase convention removes underscores from words, and capitalizes the first letter of inner words. The default camelCase versions of `blog_article` and `blog_comment` are `BlogArticle` and `BlogComment`. The name of this convention comes from the appearance of capitals inside a long word, suggestive of the humps of a camel.
  66. A table contains columns. The column value can be defined in three different ways:
  67. * If you define nothing, symfony will guess the best attributes according to the column name and a few conventions that will be described in the "Empty Columns" section later in this chapter. For instance, the `id` column in Listing 8-3 doesn't need to be defined. Symfony will make it an auto-incremented integer, primary key of the table. The article_id in the `blog_comment` table will be understood as a foreign key to the `blog_article` table (columns ending with `_id` are considered to be foreign keys, and the related table is automatically determined according to the first part of the column name). Columns called `created_at` are automatically set to the `timestamp` type. For all these columns, you don't need to specify any type. This is one of the reasons why `schema.yml` is so easy to write.
  68. * If you define only one attribute, it is the column type. Symfony understands the usual column types: `boolean`, `integer`, `float`, `date`, `varchar(size)`, `longvarchar` (converted, for instance, to `text` in MySQL), and so on. For text content over 256 characters, you need to use the `longvarchar` type, which has no size (but cannot exceed 65KB in MySQL). Note that the `date` and `timestamp` types have the usual limitations of Unix dates and cannot be set to a date prior to 1970-01-01. As you may need to set older dates (for instance, for dates of birth), a format of dates "before Unix" can be used with `bu_date` and `bu_timestamp`.
  69. * If you need to define other column attributes (like default value, required, and so on), you should write the column attributes as a set of `key: value`. This extended schema syntax is described later in the chapter.
  70. Columns can also have a `phpName` attribute, which is the capitalized version of the name (`Id`, `Title`, `Content`, and so on) and doesn't need overriding in most cases.
  71. Tables can also contain explicit foreign keys and indexes, as well as a few database-specific structure definitions. Refer to the "Extended Schema Syntax" section later in this chapter to learn more.
  72. Model Classes
  73. -------------
  74. The schema is used to build the model classes of the ORM layer. To save execution time, these classes are generated with a command-line task called `propel:build-model`.
  75. > php symfony propel:build-model
  76. >**TIP**
  77. >After building your model, you must remember to clear symfony's internal cache with `php symfony cc` so symfony can find your newly created models.
  78. Typing this command will launch the analysis of the schema and the generation of base data model classes in the `lib/model/om/` directory of your project:
  79. * `BaseArticle.php`
  80. * `BaseArticlePeer.php`
  81. * `BaseComment.php`
  82. * `BaseCommentPeer.php`
  83. In addition, the actual data model classes will be created in `lib/model/`:
  84. * `Article.php`
  85. * `ArticlePeer.php`
  86. * `Comment.php`
  87. * `CommentPeer.php`
  88. You defined only two tables, and you end up with eight files. There is nothing wrong, but it deserves some explanation.
  89. ### Base and Custom Classes
  90. Why keep two versions of the data object model in two different directories?
  91. You will probably need to add custom methods and properties to the model objects (think about the `getName()` method in Listing 8-1). But as your project develops, you will also add tables or columns. Whenever you change the `schema.yml` file, you need to regenerate the object model classes by making a new call to propel-build-model. If your custom methods were written in the classes actually generated, they would be erased after each generation.
  92. The `Base` classes kept in the `lib/model/om/` directory are the ones directly generated from the schema. You should never modify them, since every new build of the model will completely erase these files.
  93. On the other hand, the custom object classes, kept in the `lib/model/` directory, actually inherit from the `Base` ones. When the `propel:build-model` task is called on an existing model, these classes are not modified. So this is where you can add custom methods.
  94. Listing 8-4 presents an example of a custom model class as created by the first call to the `propel:build-model` task.
  95. Listing 8-4 - Sample Model Class File, in `lib/model/Article.php`
  96. [php]
  97. class Article extends BaseArticle
  98. {
  99. }
  100. It inherits all the methods of the `BaseArticle` class, but a modification in the schema will not affect it.
  101. The mechanism of custom classes extending base classes allows you to start coding, even without knowing the final relational model of your database. The related file structure makes the model both customizable and evolutionary.
  102. ### Object and Peer Classes
  103. `Article` and `Comment` are object classes that represent a record in the database. They give access to the columns of a record and to related records. This means that you will be able to know the title of an article by calling a method of an Article object, as in the example shown in Listing 8-5.
  104. Listing 8-5 - Getters for Record Columns Are Available in the Object Class
  105. [php]
  106. $article = new Article();
  107. // ...
  108. $title = $article->getTitle();
  109. `ArticlePeer` and `CommentPeer` are peer classes; that is, classes that contain static methods to operate on the tables. They provide a way to retrieve records from the tables. Their methods usually return an object or a collection of objects of the related object class, as shown in Listing 8-6.
  110. Listing 8-6 - Static Methods to Retrieve Records Are Available in the Peer Class
  111. [php]
  112. // $articles is an array of objects of class Article
  113. $articles = ArticlePeer::retrieveByPks(array(123, 124, 125));
  114. >**NOTE**
  115. >From a data model point of view, there cannot be any peer object. That's why the methods of the peer classes are called with a `::` (for static method call), instead of the usual `->` (for instance method call).
  116. So combining object and peer classes in a base and a custom version results in four classes generated per table described in the schema. In fact, there is a fifth class created in the `lib/model/map/` directory, which contains metadata information about the table that is needed for the runtime environment. But as you will probably never change this class, you can forget about it.
  117. Accessing Data
  118. --------------
  119. In symfony, your data is accessed through objects. If you are used to the relational model and using SQL to retrieve and alter your data, the object model methods will likely look complicated. But once you've tasted the power of object orientation for data access, you will probably like it a lot.
  120. But first, let's make sure we share the same vocabulary. Relational and object data model use similar concepts, but they each have their own nomenclature:
  121. Relational | Object-Oriented
  122. ------------- | ---------------
  123. Table | Class
  124. Row, record | Object
  125. Field, column | Property
  126. ### Retrieving the Column Value
  127. When symfony builds the model, it creates one base object class for each of the tables defined in the `schema.yml`. Each of these classes comes with default constructors, accessors, and mutators based on the column definitions: The `new`, `getXXX()`, and `setXXX()` methods help to create objects and give access to the object properties, as shown in Listing 8-7.
  128. Listing 8-7 - Generated Object Class Methods
  129. [php]
  130. $article = new Article();
  131. $article->setTitle('My first article');
  132. $article->setContent('This is my very first article.\n Hope you enjoy it!');
  133. $title = $article->getTitle();
  134. $content = $article->getContent();
  135. >**NOTE**
  136. >The generated object class is called `Article`, which is the `phpName` given to the `blog_article` table. If the `phpName` were not defined in the schema, the class would have been called `BlogArticle`. The accessors and mutators use a camelCase variant of the column names, so the `getTitle()` method retrieves the value of the `title` column.
  137. To set several fields at one time, you can use the `fromArray()` method, also generated for each object class, as shown in Listing 8-8.
  138. Listing 8-8 - The `fromArray()` Method Is a Multiple Setter
  139. [php]
  140. $article->fromArray(array(
  141. 'title' => 'My first article',
  142. 'content' => 'This is my very first article.\n Hope you enjoy it!',
  143. ));
  144. ### Retrieving Related Records
  145. The `article_id` column in the `blog_comment` table implicitly defines a foreign key to the `blog_article` table. Each comment is related to one article, and one article can have many comments. The generated classes contain five methods translating this relationship in an object-oriented way, as follows:
  146. * `$comment->getArticle()`: To get the related `Article` object
  147. * `$comment->getArticleId()`: To get the ID of the related `Article` object
  148. * `$comment->setArticle($article)`: To define the related `Article` object
  149. * `$comment->setArticleId($id)`: To define the related `Article` object from an ID
  150. * `$article->getComments()`: To get the related `Comment` objects
  151. The `getArticleId()` and `setArticleId()` methods show that you can consider the article_id column as a regular column and set the relationships by hand, but they are not very interesting. The benefit of the object-oriented approach is much more apparent in the three other methods. Listing 8-9 shows how to use the generated setters.
  152. Listing 8-9 - Foreign Keys Are Translated into a Special Setter
  153. [php]
  154. $comment = new Comment();
  155. $comment->setAuthor('Steve');
  156. $comment->setContent('Gee, dude, you rock: best article ever!');
  157. // Attach this comment to the previous $article object
  158. $comment->setArticle($article);
  159. // Alternative syntax
  160. // Only makes sense if the object is already saved in the database
  161. $comment->setArticleId($article->getId());
  162. Listing 8-10 shows how to use the generated getters. It also demonstrates how to chain method calls on model objects.
  163. Listing 8-10 - Foreign Keys Are Translated into Special Getters
  164. [php]
  165. // Many to one relationship
  166. echo $comment->getArticle()->getTitle();
  167. => My first article
  168. echo $comment->getArticle()->getContent();
  169. => This is my very first article.
  170. Hope you enjoy it!
  171. // One to many relationship
  172. $comments = $article->getComments();
  173. The `getArticle()` method returns an object of class `Article`, which benefits from the `getTitle()` accessor. This is much better than doing the join yourself, which may take a few lines of code (starting from the `$comment->getArticleId()` call).
  174. The `$comments` variable in Listing 8-10 contains an array of objects of class `Comment`. You can display the first one with `$comments[0]` or iterate through the collection with `foreach ($comments as $comment)`.
  175. >**NOTE**
  176. >Objects from the model are defined with a singular name by convention, and you can now understand why. The foreign key defined in the `blog_comment` table causes the creation of a `getComments()` method, named by adding an `s` to the `Comment` object name. If you gave the model object a plural name, the generation would lead to a method named `getCommentss()`, which doesn't make sense.
  177. ### Saving and Deleting Data
  178. By calling the `new` constructor, you created a new object, but not an actual record in the `blog_article` table. Modifying the object has no effect on the database either. In order to save the data into the database, you need to call the `save()` method of the object.
  179. [php]
  180. $article->save();
  181. The ORM is smart enough to detect relationships between objects, so saving the `$article` object also saves the related `$comment` object. It also knows if the saved object has an existing counterpart in the database, so the call to `save()` is sometimes translated in SQL by an `INSERT`, and sometimes by an `UPDATE`. The primary key is automatically set by the `save()` method, so after saving, you can retrieve the new primary key with `$article->getId()`.
  182. >**TIP**
  183. >You can check if an object is new by calling isNew(). And if you wonder if an object has been modified and deserves saving, call its `isModified()` method.
  184. If you read comments to your articles, you might change your mind about the interest of publishing on the Internet. And if you don't appreciate the irony of article reviewers, you can easily delete the comments with the `delete()` method, as shown in Listing 8-11.
  185. Listing 8-11 - Delete Records from the Database with the `delete()`Method on the Related Object
  186. [php]
  187. foreach ($article->getComments() as $comment)
  188. {
  189. $comment->delete();
  190. }
  191. >**TIP**
  192. >Even after calling the `delete()` method, an object remains available until the end of the request. To determine if an object is deleted in the database, call the `isDeleted()` method.
  193. ### Retrieving Records by Primary Key
  194. If you know the primary key of a particular record, use the `retrieveByPk()` class method of the peer class to get the related object.
  195. [php]
  196. $article = ArticlePeer::retrieveByPk(7);
  197. The `schema.yml` file defines the `id` field as the primary key of the `blog_article` table, so this statement will actually return the article that has `id` 7. As you used the primary key, you know that only one record will be returned; the `$article` variable contains an object of class `Article`.
  198. In some cases, a primary key may consist of more than one column. In those cases, the `retrieveByPK()` method accepts multiple parameters, one for each primary key column.
  199. You can also select multiple objects based on their primary keys, by calling the generated `retrieveByPKs()` method, which expects an array of primary keys as a parameter.
  200. ### Retrieving Records with Criteria
  201. When you want to retrieve more than one record, you need to call the `doSelect()` method of the peer class corresponding to the objects you want to retrieve. For instance, to retrieve objects of class `Article`, call `ArticlePeer::doSelect()`.
  202. The first parameter of the `doSelect()` method is an object of class `Criteria`, which is a simple query definition class defined without SQL for the sake of database abstraction.
  203. An empty `Criteria` returns all the objects of the class. For instance, the code shown in Listing 8-12 retrieves all the articles.
  204. Listing 8-12 - Retrieving Records by Criteria with `doSelect()`--Empty Criteria
  205. [php]
  206. $c = new Criteria();
  207. $articles = ArticlePeer::doSelect($c);
  208. // Will result in the following SQL query
  209. SELECT blog_article.ID, blog_article.TITLE, blog_article.CONTENT,
  210. blog_article.CREATED_AT
  211. FROM blog_article;
  212. >**SIDEBAR**
  213. >Hydrating
  214. >
  215. >The call to `::doSelect()` is actually much more powerful than a simple SQL query. First, the SQL is optimized for the DBMS you choose. Second, any value passed to the `Criteria` is escaped before being integrated into the SQL code, which prevents SQL injection risks. Third, the method returns an array of objects, rather than a result set. The ORM automatically creates and populates objects based on the database result set. This process is called hydrating.
  216. For a more complex object selection, you need an equivalent of the WHERE, ORDER BY, GROUP BY, and other SQL statements. The `Criteria` object has methods and parameters for all these conditions. For example, to get all comments written by Steve, ordered by date, build a `Criteria` as shown in Listing 8-13.
  217. Listing 8-13 - Retrieving Records by Criteria with `doSelect()`--Criteria with Conditions
  218. [php]
  219. $c = new Criteria();
  220. $c->add(CommentPeer::AUTHOR, 'Steve');
  221. $c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
  222. $comments = CommentPeer::doSelect($c);
  223. // Will result in the following SQL query
  224. SELECT blog_comment.ARTICLE_ID, blog_comment.AUTHOR, blog_comment.CONTENT,
  225. blog_comment.CREATED_AT
  226. FROM blog_comment
  227. WHERE blog_comment.author = 'Steve'
  228. ORDER BY blog_comment.CREATED_AT ASC;
  229. The class constants passed as parameters to the add() methods refer to the property names. They are named after the capitalized version of the column names. For instance, to address the `content` column of the `blog_article` table, use the `ArticlePeer::CONTENT` class constant.
  230. >**NOTE**
  231. >Why use `CommentPeer::AUTHOR` instead of `blog_comment.AUTHOR`, which is the way it will be output in the SQL query anyway? Suppose that you need to change the name of the author field to `contributor` in the database. If you used `blog_comment.AUTHOR`, you would have to change it in every call to the model. On the other hand, by using `CommentPeer::AUTHOR`, you simply need to change the column name in the `schema.yml` file, keep `phpName` as `AUTHOR`, and rebuild the model.
  232. Table 8-1 compares the SQL syntax with the `Criteria` object syntax.
  233. Table 8-1 - SQL and Criteria Object Syntax
  234. SQL | Criteria
  235. ------------------------------------------------------------ | -----------------------------------------------
  236. `WHERE column = value` | `->add(column, value);`
  237. `WHERE column <> value` | `->add(column, value, Criteria::NOT_EQUAL);`
  238. **Other Comparison Operators** |
  239. `> , <` | `Criteria::GREATER_THAN, Criteria::LESS_THAN`
  240. `>=, <=` | `Criteria::GREATER_EQUAL, Criteria::LESS_EQUAL`
  241. `IS NULL, IS NOT NULL` | `Criteria::ISNULL, Criteria::ISNOTNULL`
  242. `LIKE, ILIKE` | `Criteria::LIKE, Criteria::ILIKE`
  243. `IN, NOT IN` | `Criteria::IN, Criteria::NOT_IN`
  244. **Other SQL Keywords** |
  245. `ORDER BY column ASC` | `->addAscendingOrderByColumn(column);`
  246. `ORDER BY column DESC` | `->addDescendingOrderByColumn(column);`
  247. `LIMIT limit` | `->setLimit(limit)`
  248. `OFFSET offset` | `->setOffset(offset) `
  249. `FROM table1, table2 WHERE table1.col1 = table2.col2` | `->addJoin(col1, col2)`
  250. `FROM table1 LEFT JOIN table2 ON table1.col1 = table2.col2` | `->addJoin(col1, col2, Criteria::LEFT_JOIN)`
  251. `FROM table1 RIGHT JOIN table2 ON table1.col1 = table2.col2` | `->addJoin(col1, col2, Criteria::RIGHT_JOIN)`
  252. >**TIP**
  253. >The best way to discover and understand which methods are available in generated classes is to look at the `Base` files in the `lib/model/om/` folder after generation. The method names are pretty explicit, but if you need more comments on them, set the `propel.builder.addComments` parameter to `true` in the `config/propel.ini` file and rebuild the model.
  254. Listing 8-14 shows another example of `Criteria` with multiple conditions. It retrieves all the comments by Steve on articles containing the word "enjoy," ordered by date.
  255. Listing 8-14 - Another Example of Retrieving Records by Criteria with `doSelect()`--Criteria with Conditions
  256. [php]
  257. $c = new Criteria();
  258. $c->add(CommentPeer::AUTHOR, 'Steve');
  259. $c->addJoin(CommentPeer::ARTICLE_ID, ArticlePeer::ID);
  260. $c->add(ArticlePeer::CONTENT, '%enjoy%', Criteria::LIKE);
  261. $c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
  262. $comments = CommentPeer::doSelect($c);
  263. // Will result in the following SQL query
  264. SELECT blog_comment.ID, blog_comment.ARTICLE_ID, blog_comment.AUTHOR,
  265. blog_comment.CONTENT, blog_comment.CREATED_AT
  266. FROM blog_comment, blog_article
  267. WHERE blog_comment.AUTHOR = 'Steve'
  268. AND blog_article.CONTENT LIKE '%enjoy%'
  269. AND blog_comment.ARTICLE_ID = blog_article.ID
  270. ORDER BY blog_comment.CREATED_AT ASC
  271. Just as SQL is a simple language that allows you to build very complex queries, the Criteria object can handle conditions with any level of complexity. But since many developers think first in SQL before translating a condition into object-oriented logic, the `Criteria` object may be difficult to comprehend at first. The best way to understand it is to learn from examples and sample applications. The symfony project website, for instance, is full of `Criteria` building examples that will enlighten you in many ways.
  272. In addition to the `doSelect()` method, every peer class has a `doCount()` method, which simply counts the number of records satisfying the criteria passed as a parameter and returns the count as an integer. As there is no object to return, the hydrating process doesn't occur in this case, and the `doCount()` method is faster than `doSelect()`.
  273. The peer classes also provide `doDelete()`, `doInsert()`, and `doUpdate()` methods, which all expect a `Criteria` as a parameter. These methods allow you to issue `DELETE`, `INSERT`, and `UPDATE` queries to your database. Check the generated peer classes in your model for more details on these Propel methods.
  274. Finally, if you just want the first object returned, replace `doSelect()` with a `doSelectOne()` call. This may be the case when you know that a `Criteria` will return only one result, and the advantage is that this method returns an object rather than an array of objects.
  275. >**TIP**
  276. >When a `doSelect()` query returns a large number of results, you might want to display only a subset of it in your response. Symfony provides a pager class called sfPropelPager, which automates the pagination of results. Check the pager documentation at [http://www.symfony-project.org/cookbook/1_1/en/pager](http://www.symfony-project.org/cookbook/1_1/en/pager) for more information and usage examples.
  277. ### Using Raw SQL Queries
  278. Sometimes, you don't want to retrieve objects, but want to get only synthetic results calculated by the database. For instance, to get the latest creation date of all articles, it doesn't make sense to retrieve all the articles and to loop on the array. You will prefer to ask the database to return only the result, because it will skip the object hydrating process.
  279. On the other hand, you don't want to call the PHP commands for database management directly, because then you would lose the benefit of database abstraction. This means that you need to bypass the ORM (Propel) but not the database abstraction (Creole).
  280. Querying the database with Creole requires that you do the following:
  281. 1. Get a database connection.
  282. 2. Build a query string.
  283. 3. Create a statement out of it.
  284. 4. Iterate on the result set that results from the statement execution.
  285. If this looks like gibberish to you, the code in Listing 8-15 will probably be more explicit.
  286. Listing 8-15 - Custom SQL Query with Creole
  287. [php]
  288. $connection = Propel::getConnection();
  289. $query = 'SELECT MAX(%s) AS max FROM %s';
  290. $query = sprintf($query, ArticlePeer::CREATED_AT, ArticlePeer::TABLE_NAME);
  291. $statement = $connection->prepareStatement($query);
  292. $resultset = $statement->executeQuery();
  293. $resultset->next();
  294. $max = $resultset->getInt('max');
  295. Just like Propel selections, Creole queries are tricky when you first start using them. Once again, examples from existing applications and tutorials will show you the right way.
  296. >**CAUTION**
  297. >If you are tempted to bypass this process and access the database directly, you risk losing the security and abstraction provided by Creole. Doing it the Creole way is longer, but it forces you to use good practices that guarantee the performance, portability, and security of your application. This is especially true for queries that contain parameters coming from a untrusted source (such as an Internet user). Creole does all the necessary escaping and secures your database. Accessing the database directly puts you at risk of SQL-injection attacks.
  298. ### Using Special Date Columns
  299. Usually, when a table has a column called `created_at`, it is used to store a timestamp of the date when the record was created. The same applies to updated_at columns, which are to be updated each time the record itself is updated, to the value of the current time.
  300. The good news is that symfony will recognize the names of these columns and handle their updates for you. You don't need to manually set the `created_at` and `updated_at` columns; they will automatically be updated, as shown in Listing 8-16. The same applies for columns named `created_on` and `updated_on`.
  301. Listing 8-16 - `created_at` and `updated_at` Columns Are Dealt with Automatically
  302. [php]
  303. $comment = new Comment();
  304. $comment->setAuthor('Steve');
  305. $comment->save();
  306. // Show the creation date
  307. echo $comment->getCreatedAt();
  308. => [date of the database INSERT operation]
  309. Additionally, the getters for date columns accept a date format as an argument:
  310. [php]
  311. echo $comment->getCreatedAt('Y-m-d');
  312. >**SIDEBAR**
  313. >Refactoring to the Data layer
  314. >
  315. >When developing a symfony project, you often start by writing the domain logic code in the actions. But the database queries and model manipulation should not be stored in the controller layer. So all the logic related to the data should be moved to the model layer. Whenever you need to do the same request in more than one place in your actions, think about transferring the related code to the model. It helps to keep the actions short and readable.
  316. >
  317. >For example, imagine the code needed in a blog to retrieve the ten most popular articles for a given tag (passed as request parameter). This code should not be in an action, but in the model. In fact, if you need to display this list in a template, the action should simply look like this:
  318. >
  319. > [php]
  320. > public function executeShowPopularArticlesForTag($request)
  321. > {
  322. > $tag = TagPeer::retrieveByName($request->getParameter('tag'));
  323. > $this->foward404Unless($tag);
  324. > $this->articles = $tag->getPopularArticles(10);
  325. > }
  326. >
  327. >The action creates an object of class `Tag` from the request parameter. Then all the code needed to query the database is located in a `getPopularArticles()` method of this class. It makes the action more readable, and the model code can easily be reused in another action.
  328. >
  329. >Moving code to a more appropriate location is one of the techniques of refactoring. If you do it often, your code will be easy to maintain and to understand by other developers. A good rule of thumb about when to do refactoring to the data layer is that the code of an action should rarely contain more than ten lines of PHP code.
  330. Database Connections
  331. --------------------
  332. The data model is independent from the database used, but you will definitely use a database. The minimum information required by symfony to send requests to the project database is the name, the credentials, and the type of database. These connection settings can be configured by passing a data source name (DSN) to the `configure:database` task:
  333. > php symfony configure:database "mysql://login:passwd@localhost/blog"
  334. The connection settings are environment-dependent. You can define distinct settings for the `prod`, `dev`, and `test` environments, or any other environment in your application by using the `env` option:
  335. > php symfony --env=prod configure:database "mysql://login:passwd@localhost/blog"
  336. This configuration can also be overridden per application. For instance, you can use this approach to have different security policies for a front-end and a back-end application, and define several database users with different privileges in your database to handle this:
  337. > php symfony --app=frontend configure:database "mysql://login:passwd@localhost/blog"
  338. For each environment, you can define many connections. Each connection refers to a schema being labeled with the same name. The default connection name used is `propel` and it refers to the `propel` schema in Listing 8-3. The `name` option allows you to create another connection:
  339. > php symfony --name=main configure:database "mysql://login:passwd@localhost/blog"
  340. You can also enter these connection settings manually in the `databases.yml` file located in the `config/` directory. Listing 8-17 shows an example of such a file and Listing 8-18 shows the same example with the extended notation.
  341. Listing 8-17 - Shorthand Database Connection Settings
  342. [yml]
  343. all:
  344. propel:
  345. class: sfPropelDatabase
  346. param:
  347. dsn: mysql://login:passwd@localhost/blog
  348. Listing 8-18 - Sample Database Connection Settings, in `myproject/config/databases.yml`
  349. [yml]
  350. prod:
  351. propel:
  352. param:
  353. hostspec: mydataserver
  354. username: myusername
  355. password: xxxxxxxxxx
  356. all:
  357. propel:
  358. class: sfPropelDatabase
  359. param:
  360. phptype: mysql # Database vendor
  361. hostspec: localhost
  362. database: blog
  363. username: login
  364. password: passwd
  365. port: 80
  366. encoding: utf8 # Default charset for table creation
  367. persistent: true # Use persistent connections
  368. The permitted values of the `phptype` parameter are the ones of the database systems supported by Creole:
  369. * `mysql`
  370. * `mssql`
  371. * `pgsql`
  372. * `sqlite`
  373. * `oracle`
  374. `hostspec`, `database`, `username`, and `password` are the usual database connection settings.
  375. To override the configuration per application, you need to edit an application-specific file, such as `apps/frontend/config/databases.yml`.
  376. If you use a SQLite database, the `hostspec` parameter must be set to the path of the database file. For instance, if you keep your blog database in `data/blog.db`, the `databases.yml` file will look like Listing 8-19.
  377. Listing 8-19 - Database Connection Settings for SQLite Use a File Path As Host
  378. [yml]
  379. all:
  380. propel:
  381. class: sfPropelDatabase
  382. param:
  383. phptype: sqlite
  384. database: %SF_DATA_DIR%/blog.db
  385. Extending the Model
  386. -------------------
  387. The generated model methods are great but often not sufficient. As soon as you implement your own business logic, you need to extend it, either by adding new methods or by overriding existing ones.
  388. ### Adding New Methods
  389. You can add new methods to the empty model classes generated in the `lib/model/` directory. Use `$this` to call methods of the current object, and use `self::` to call static methods of the current class. Remember that the custom classes inherit methods from the `Base` classes located in the `lib/model/om/` directory.
  390. For instance, for the `Article` object generated based on Listing 8-3, you can add a magic `__toString()` method so that echoing an object of class `Article` displays its title, as shown in Listing 8-20.
  391. Listing 8-20 - Customizing the Model, in `lib/model/Article.php`
  392. [php]
  393. class Article extends BaseArticle
  394. {
  395. public function __toString()
  396. {
  397. return $this->getTitle(); // getTitle() is inherited from BaseArticle
  398. }
  399. }
  400. You can also extend the peer classes--for instance, to add a method to retrieve all articles ordered by creation date, as shown in Listing 8-21.
  401. Listing 8-21 - Customizing the Model, in `lib/model/ArticlePeer.php`
  402. [php]
  403. class ArticlePeer extends BaseArticlePeer
  404. {
  405. public static function getAllOrderedByDate()
  406. {
  407. $c = new Criteria();
  408. $c->addAscendingOrderByColumn(self::CREATED_AT);
  409. return self::doSelect($c);
  410. }
  411. }
  412. The new methods are available in the same way as the generated ones, as shown in Listing 8-22.
  413. Listing 8-22 - Using Custom Model Methods Is Like Using the Generated Methods
  414. [php]
  415. foreach (ArticlePeer::getAllOrderedByDate() as $article)
  416. {
  417. echo $article; // Will call the magic __toString() method
  418. }
  419. ### Overriding Existing Methods
  420. If some of the generated methods in the `Base` classes don't fit your requirements, you can still override them in the custom classes. Just make sure that you use the same method signature (that is, the same number of arguments).
  421. For instance, the `$article->getComments()` method returns an array of `Comment` objects, in no particular order. If you want to have the results ordered by creation date, with the latest comment coming first, then override the `getComments()` method, as shown in Listing 8-23. Be aware that the original `getComments()` method (found in `lib/model/om/BaseArticle.php`) expects a criteria value and a connection value as parameters, so your function must do the same.
  422. Listing 8-23 - Overriding Existing Model Methods, in `lib/model/Article.php`
  423. [php]
  424. public function getComments($criteria = null, $con = null)
  425. {
  426. if (is_null($criteria))
  427. {
  428. $criteria = new Criteria();
  429. }
  430. else
  431. {
  432. // Objects are passed by reference in PHP5, so to avoid modifying the original, you must clone it
  433. $criteria = clone $criteria;
  434. }
  435. $criteria->addDescendingOrderByColumn(CommentPeer::CREATED_AT);
  436. return parent::getComments($criteria, $con);
  437. }
  438. The custom method eventually calls the one of the parent Base class, and that's good practice. However, you can completely bypass it and return the result you want.
  439. ### Using Model Behaviors
  440. Some model modifications are generic and can be reused. For instance, methods to make a model object sortable and an optimistic lock to prevent conflicts between concurrent object saving are generic extensions that can be added to many classes.
  441. Symfony packages these extensions into behaviors. Behaviors are external classes that provide additional methods to model classes. The model classes already contain hooks, and symfony knows how to extend them by way of `sfMixer` (see Chapter 17 for details).
  442. To enable behaviors in your model classes, you must modify one setting in the `config/propel.ini` file:
  443. propel.builder.AddBehaviors = true // Default value is false
  444. There is no behavior bundled by default in symfony, but they can be installed via plug-ins. Once a behavior plug-in is installed, you can assign the behavior to a class with a single line. For instance, if you install the sfPropelParanoidBehaviorPlugin in your application, you can extend an `Article` class with this behavior by adding the following at the end of the `Article.class.php`:
  445. [php]
  446. sfPropelBehavior::add('Article', array(
  447. 'paranoid' => array('column' => 'deleted_at')
  448. ));
  449. After rebuilding the model, deleted `Article` objects will remain in the database, invisible to the queries using the ORM, unless you temporarily disable the behavior with `sfPropelParanoidBehavior::disable()`.
  450. **New in symfony 1.1**: Alternatively, you can also declare behaviors directly in the `schema.yml`, by listing them under the `_behaviors` key (see Listing 8-34 below).
  451. Check the list of symfony plug-ins in the wiki to find behaviors ([http://trac.symfony-project.org/wiki/SymfonyPlugins#Behaviors](http://trac.symfony-project.org/wiki/SymfonyPlugins#Behaviors)). Each has its own documentation and installation guide.
  452. Extended Schema Syntax
  453. ----------------------
  454. A `schema.yml` file can be simple, as shown in Listing 8-3. But relational models are often complex. That's why the schema has an extensive syntax able to handle almost every case.
  455. ### Attributes
  456. Connections and tables can have specific attributes, as shown in Listing 8-24. They are set under an `_attributes` key.
  457. Listing 8-24 - Attributes for Connections and Tables
  458. [yml]
  459. propel:
  460. _attributes: { noXsd: false, defaultIdMethod: none, package: lib.model }
  461. blog_article:
  462. _attributes: { phpName: Article }
  463. You may want your schema to be validated before code generation takes place. To do that, deactivate the `noXSD` attribute for the connection. The connection also supports the `defaultIdMethod` attribute. If none is provided, then the database's native method of generating IDs will be used--for example, `autoincrement` for MySQL, or `sequences` for PostgreSQL. The other possible value is `none`.
  464. The `package` attribute is like a namespace; it determines the path where the generated classes are stored. It defaults to `lib/model/`, but you can change it to organize your model in subpackages. For instance, if you don't want to mix the core business classes and the classes defining a database-stored statistics engine in the same directory, then define two schemas with `lib.model.business` and `lib.model.stats` packages.
  465. You already saw the `phpName` table attribute, used to set the name of the generated class mapping the table.
  466. Tables that contain localized content (that is, several versions of the content, in a related table, for internationalization) also take two additional attributes (see Chapter 13 for details), as shown in Listing 8-25.
  467. Listing 8-25 - Attributes for i18n Tables
  468. [yml]
  469. propel:
  470. blog_article:
  471. _attributes: { isI18N: true, i18nTable: db_group_i18n }
  472. >**SIDEBAR**
  473. >Dealing with multiple schemas
  474. >
  475. >You can have more than one schema per application. Symfony will take into account every file ending with `schema.yml` or `schema.xml` in the `config/` folder. If your application has many tables, or if some tables don't share the same connection, you will find this approach very useful.
  476. >
  477. >Consider these two schemas:
  478. >
  479. > [yml]
  480. > // In config/business-schema.yml
  481. > propel:
  482. > blog_article:
  483. > _attributes: { phpName: Article }
  484. > id:
  485. > title: varchar(50)
  486. >
  487. > // In config/stats-schema.yml
  488. > propel:
  489. > stats_hit:
  490. > _attributes: { phpName: Hit }
  491. > id:
  492. > resource: varchar(100)
  493. > created_at:
  494. >
  495. >
  496. >Both schemas share the same connection (`propel`), and the `Article` and `Hit` classes will be generated under the same `lib/model/` directory. Everything happens as if you had written only one schema.
  497. >
  498. >You can also have different schemas use different connections (for instance, `propel` and `propel_bis`, to be defined in `databases.yml`) and organize the generated classes in subdirectories:
  499. >
  500. >
  501. > [yml]
  502. > // In config/business-schema.yml
  503. > propel:
  504. > blog_article:
  505. > _attributes: { phpName: Article, package: lib.model.business }
  506. > id:
  507. > title: varchar(50)
  508. >
  509. > // In config/stats-schema.yml
  510. > propel_bis:
  511. > stats_hit:
  512. > _attributes: { phpName: Hit, package: lib.model.stat }
  513. > id:
  514. > resource: varchar(100)
  515. > created_at:
  516. >
  517. >
  518. >Many applications use more than one schema. In particular, some plug-ins have their own schema and package to avoid messing with your own classes (see Chapter 17 for details).
  519. ### Column Details
  520. The basic syntax gives you two choices: let symfony deduce the column characteristics from its name (by giving an empty value) or define the type with one of the type keywords. Listing 8-26 demonstrates these choices.
  521. Listing 8-26 - Basic Column Attributes
  522. [yml]
  523. propel:
  524. blog_article:
  525. id: # Let symfony do the work
  526. title: varchar(50) # Specify the type yourself
  527. But you can define much more for a column. If you do, you will need to define column settings as an associative array, as shown in Listing 8-27.
  528. Listing 8-27 - Complex Column Attributes
  529. [yml]
  530. propel:
  531. blog_article:
  532. id: { type: integer, required: true, primaryKey: true, autoIncrement: true }
  533. name: { type: varchar(50), default: foobar, index: true }
  534. group_id: { type: integer, foreignTable: db_group, foreignReference: id, onDelete: cascade }
  535. The column parameters are as follows:
  536. * `type`: Column type. The choices are `boolean`, `tinyint`, `smallint`, `integer`, `bigint`, `double`, `float`, `real`, `decimal`, `char`, `varchar(size)`, `longvarchar`, `date`, `time`, `timestamp`, `bu_date`, `bu_timestamp`, `blob`, and `clob`.
  537. * `required`: Boolean. Set it to `true` if you want the column to be required.
  538. * `size`: The size or length of the field for types that support it
  539. * `scale`: Number of decimal places for use with decimal data type (size must also be specified)
  540. * `default`: Default value.
  541. * `primaryKey`: Boolean. Set it to `true` for primary keys.
  542. * `autoIncrement`: Boolean. Set it to `true` for columns of type `integer` that need to take an auto-incremented value.
  543. * `sequence`: Sequence name for databases using sequences for `autoIncrement` columns (for example, PostgreSQL and Oracle).
  544. * `index`: Boolean. Set it to `true` if you want a simple index or to `unique` if you want a unique index to be created on the column.
  545. * `foreignTable`: A table name, used to create a foreign key to another table.
  546. * `foreignReference`: The name of the related column if a foreign key is defined via `foreignTable`.
  547. * `onDelete`: Determines the action to trigger when a record in a related table is deleted. When set to `setnull`, the foreign key column is set to `null`. When set to `cascade`, the record is deleted. If the database engine doesn't support the set behavior, the ORM emulates it. This is relevant only for columns bearing a `foreignTable` and a `foreignReference`.
  548. * `isCulture`: Boolean. Set it to `true` for culture columns in localized content tables (see Chapter 13).
  549. ### Foreign Keys
  550. As an alternative to the `foreignTable` and `foreignReference` column attributes, you can add foreign keys under the `_foreignKeys:` key in a table. The schema in Listing 8-28 will create a foreign key on the `user_id` column, matching the `id` column in the `blog_user` table.
  551. Listing 8-28 - Foreign Key Alternative Syntax
  552. [yml]
  553. propel:
  554. blog_article:
  555. id:
  556. title: varchar(50)
  557. user_id: { type: integer }
  558. _foreignKeys:
  559. -
  560. foreignTable: blog_user
  561. onDelete: cascade
  562. references:
  563. - { local: user_id, foreign: id }
  564. The alternative syntax is useful for multiple-reference foreign keys and to give foreign keys a name, as shown in Listing 8-29.
  565. Listing 8-29 - Foreign Key Alternative Syntax Applied to Multiple Reference Foreign Key
  566. _foreignKeys:
  567. my_foreign_key:
  568. foreignTable: db_user
  569. onDelete: cascade
  570. references:
  571. - { local: user_id, foreign: id }
  572. - { local: post_id, foreign: id }
  573. ### Indexes
  574. As an alternative to the `index` column attribute, you can add indexes under the `_indexes:` key in a table. If you want to define unique indexes, you must use the `_uniques:` header instead. For columns that require a size, because they are text columns, the size of the index is specified the same way as the length of the column using parentheses. Listing 8-30 shows the alternative syntax for indexes.
  575. Listing 8-30 - Indexes and Unique Indexes Alternative Syntax
  576. [yml]
  577. propel:
  578. blog_article:
  579. id:
  580. title: varchar(50)
  581. created_at:
  582. _indexes:
  583. my_index: [title(10), user_id]
  584. _uniques:
  585. my_other_index: [created_at]
  586. The alternative syntax is useful only for indexes built on more than one column.
  587. ### Empty Columns
  588. When meeting a column with no value, symfony will do some magic and add a value of its own. See Listing 8-31 for the details added to empty columns.
  589. Listing 8-31 - Column Details Deduced from the Column Name
  590. // Empty columns named id are considered primary keys
  591. id: { type: integer, required: true, primaryKey: true, autoIncrement: true }
  592. // Empty columns named XXX_id are considered foreign keys
  593. foobar_id: { type: integer, foreignTable: db_foobar, foreignReference: id }
  594. // Empty columns named created_at, updated at, created_on and updated_on
  595. // are considered dates and automatically take the timestamp type
  596. created_at: { type: timestamp }
  597. updated_at: { type: timestamp }
  598. For foreign keys, symfony will look for a table having the same `phpName` as the beginning of the column name, and if one is found, it will take this table name as the `foreignTable`.
  599. ### I18n Tables
  600. Symfony supports content internationalization in related tables. This means that when you have content subject to internationalization, it is stored in two separate tables: one with the invariable columns and another with the internationalized columns.
  601. In a `schema.yml` file, all that is implied when you name a table `foobar_i18n`. For instance, the schema shown in Listing 8-32 will be automatically completed with columns and table attributes to make the internationalized content mechanism work. Internally, symfony will understand it as if it were written like Listing 8-33. Chapter 13 will tell you more about i18n.
  602. Listing 8-32 - Implied i18n Mechanism
  603. [yml]
  604. propel:
  605. db_group:
  606. id:
  607. created_at:
  608. db_group_i18n:
  609. name: varchar(50)
  610. Listing 8-33 - Explicit i18n Mechanism
  611. [yml]
  612. propel:
  613. db_group:
  614. _attributes: { isI18N: true, i18nTable: db_group_i18n }
  615. id:
  616. created_at:
  617. db_group_i18n:
  618. id: { type: integer, required: true, primaryKey: true,foreignTable: db_group, foreignReference: id, onDelete: cascade }
  619. culture: { isCulture: true, type: varchar(7), required: true,primaryKey: true }
  620. name: varchar(50)
  621. ### Behaviors (new in symfony 1.1)
  622. Behaviors are model modifiers provided by plug-ins that add new capabilities to your Propel classes. Chapter 17 explains more about behaviors. You can define behaviors right in the schema, by listing them for each table, together with their parameters, under the `_behaviors` key. Listing 8-34 gives an example by extending the `BlogArticle` class with the `paranoid` behavior.
  623. Listing 8-34 - Behaviors Declaration
  624. [yml]
  625. propel:
  626. blog_article:
  627. title: varchar(50)
  628. _behaviors:
  629. paranoid: { column: deleted_at }
  630. ### Beyond the schema.yml: The schema.xml
  631. As a matter of fact, the schema.yml format is internal to symfony. When you call a propel- command, symfony actually translates this file into a `generated-schema.xml` file, which is the type of file expected by Propel to actually perform tasks on the model.
  632. The `schema.xml` file contains the same information as its YAML equivalent. For example, Listing 8-3 is converted to the XML file shown in Listing 8-35.
  633. Listing 8-35 - Sample `schema.xml`, Corresponding to Listing 8-3
  634. [xml]
  635. <?xml version="1.0" encoding="UTF-8"?>
  636. <database name="propel" defaultIdMethod="native" noXsd="true" package="lib.model">
  637. <table name="blog_article" phpName="Article">
  638. <column name="id" type="integer" required="true" primaryKey="true"autoIncrement="true" />
  639. <column name="title" type="varchar" size="255" />
  640. <column name="content" type="longvarchar" />
  641. <column name="created_at" type="timestamp" />
  642. </table>
  643. <table name="blog_comment" phpName="Comment">
  644. <column name="id" type="integer" required="true" primaryKey="true"autoIncrement="true" />
  645. <column name="article_id" type="integer" />
  646. <foreign-key foreignTable="blog_article">
  647. <reference local="article_id" foreign="id"/>
  648. </foreign-key>
  649. <column name="author" type="varchar" size="255" />
  650. <column name="content" type="longvarchar" />
  651. <column name="created_at" type="timestamp" />
  652. </table>
  653. </database>
  654. The description of the `schema.xml` format can be found in the documentation and the "Getting Started" sections of the Propel project website ([http://propel.phpdb.org/docs/user_guide/chapters/appendices/AppendixB-SchemaReference.html](http://propel.phpdb.org/docs/user_guide/chapters/appendices/AppendixB-SchemaReference.html)).
  655. The YAML format was designed to keep the schemas simple to read and write, but the trade-off is that the most complex schemas can't be described with a `schema.yml` file. On the other hand, the XML format allows for full schema description, whatever its complexity, and includes database vendor-specific settings, table inheritance, and so on.
  656. Symfony actually understands schemas written in XML format. So if your schema is too complex for the YAML syntax, if you have an existing XML schema, or if you are already familiar with the Propel XML syntax, you don't have to switch to the symfony YAML syntax. Place your `schema.xml` in the project `config/` directory, build the model, and there you go.
  657. >**SIDEBAR**
  658. >Propel in symfony
  659. >
  660. >All the details given in this chapter are not specific to symfony, but rather to Propel. Propel is the preferred object/relational abstraction layer for symfony, but you can choose an alternative one. However, symfony works more seamlessly with Propel, for the following reasons:
  661. >
  662. >All the object data model classes and the `Criteria` class are autoloading classes. As soon as you use them, symfony will include the right files, and you don't need to manually add the file inclusion statements. In symfony, Propel doesn't need to be launched nor initialized. When an object uses Propel, the library initiates by itself. Some symfony helpers use Propel objects as parameters to achieve high-level tasks (such as pagination or filtering). Propel objects allow rapid prototyping and generation of a backend for your application (Chapter 14 provides more details). The schema is faster to write through the `schema.yml` file.
  663. >
  664. >And, as Propel is independent of the database used, so is symfony.
  665. Don't Create the Model Twice
  666. ----------------------------
  667. The trade-off of using an ORM is that you must define the data structure twice: once for the database, and once for the object model. Fortunately, symfony offers command-line tools to generate one based on the other, so you can avoid duplicate work.
  668. ### Building a SQL Database Structure Based on an Existing Schema
  669. If you start your application by writing the `schema.yml` file, symfony can generate a SQL query that creates the tables directly from the YAML data model. To use the query, go to your root project directory and type this:
  670. > php symfony propel:build-sql
  671. A `lib.model.schema.sql` file will be created in `myproject/data/sql/`. Note that the generated SQL code will be optimized for the database system defined in the `phptype` parameter of the `propel.ini` file.
  672. You can use the schema.sql file directly to build the tables. For instance, in MySQL, type this:
  673. > mysqladmin -u root -p create blog
  674. > mysql -u root -p blog < data/sql/lib.model.schema.sql
  675. The generated SQL is also helpful to rebuild the database in another environment, or to change to another DBMS. If the connection settings are properly defined in your `propel.ini`, you can even use the `php symfony propel:insert-sql` command to do this automatically.
  676. >**TIP**
  677. >The command line also offers a task to populate your database with data based on a text file. See Chapter 16 for more information about the `propel:data-load` task and the YAML fixture files.
  678. ### Generating a YAML Data Model from an Existing Database
  679. Symfony can use the Creole database access layer to generate a `schema.yml` file from an existing database, thanks to introspection (the capability of databases to determine the structure of the tables on which they are operating). This can be particularly useful when you do reverse-engineering, or if you prefer working on the database before working on the object model.
  680. In order to do this, you need to make sure that the project `propel.ini` file points to the correct database and contains all connection settings, and then call the `propel:build-schema` command:
  681. > php symfony propel:build-schema
  682. A brand-new `schema.yml` file built from your database structure is generated in the `config/` directory. You can build your model based on this schema.
  683. The schema-generation command is quite powerful and can add a lot of database-dependent information to your schema. As the YAML format doesn't handle this kind of vendor information, you need to generate an XML schema to take advantage of it. You can do this simply by adding an `xml` argument to the `build-schema` task:
  684. > php symfony propel:build-schema --xml
  685. Instead of generating a `schema.yml` file, this will create a `schema.xml` file fully compatible with Propel, containing all the vendor information. But be aware that generated XML schemas tend to be quite verbose and difficult to read.
  686. >**SIDEBAR**
  687. >The `propel.ini` Configuration
  688. >
  689. >The `propel:build-sql` and `propel:build-schema` tasks don't use the connection settings defined in the `databases.yml` file. Rather, these tasks use the connection settings in another file, called `propel.ini` and stored in the project `config/` directory:
  690. >
  691. >
  692. > propel.database.createUrl = mysql://login:passwd@localhost
  693. > propel.database.url = mysql://login:passwd@localhost/blog
  694. >
  695. >
  696. >This file contains other settings used to configure the Propel generator to make generated model classes compatible with symfony. Most settings are internal and of no interest to the user, apart from a few:
  697. >
  698. >
  699. > // Base classes are autoloaded in symfony
  700. > // Set this to true to use include_once statements instead
  701. > // (Small negative impact on performance)
  702. > propel.builder.addIncludes = false
  703. >
  704. > // Generated classes are not commented by default
  705. > // Set this to true to add comments to Base classes
  706. > // (Small negative impact on performance)
  707. > propel.builder.addComments = false
  708. >
  709. > // Behaviors are not handled by default
  710. > // Set this to true to be able to handle them
  711. > propel.builder.AddBehaviors = false
  712. >
  713. >
  714. >After you make a modification to the `propel.ini` settings, don't forget to rebuild the model so the changes will take effect.
  715. Summary
  716. -------
  717. Symfony uses Propel as the ORM and Creole as the database abstraction layer. It means that you must first describe the relational schema of your database in YAML before generating the object model classes. Then, at runtime, use the methods of the object and peer classes to retrieve information about a record or a set of records. You can override them and extend the model easily by adding methods to the custom classes. The connection settings are defined in a `databases.yml` file, which can support more than one connection. And the command line contains special tasks to avoid duplicate structure definition.
  718. The model layer is the most complex of the symfony framework. One reason for this complexity is that data manipulation is an intricate matter. The related security issues are crucial for a website and should not be ignored. Another reason is that symfony is more suited for middle- to large-scale applications in an enterprise context. In such applications, the automations provided by the symfony model really represent a gain of time, worth the investment in learning its internals.
  719. So don't hesitate to spend some time testing the model objects and methods to fully understand them. The solidity and scalability of your applications will be a great reward.