CC-2166: Packaging Improvements. Moved the Zend app into airtime_mvc. It is now installed to /var/www/airtime. Storage is now set to /srv/airtime/stor. Utils are now installed to /usr/lib/airtime/utils/. Added install/airtime-dircheck.php as a simple test to see if everything is install/uninstalled correctly.

This commit is contained in:
Paul Baranowski 2011-04-14 18:55:04 -04:00
parent 514777e8d2
commit b11cbd8159
4546 changed files with 138 additions and 51 deletions

View file

@ -0,0 +1,34 @@
= Adding Additional SQL Files =
In many cases you may wish to have the ''insert-sql'' task perform additional SQL operations (e.g. add views, stored procedures, triggers, sample data, etc.). Rather than have to run additional SQL statements yourself every time you re-build your object model, you can have the Propel generator do this for you.
== 1. Create the SQL DDL files ==
Create any additional SQL files that you want executed against the database (after the base ''schema.sql'' file is applied).
For example, if we wanted to add a default value to a column that was unsupported in the schema (e.g. where value is a SQL function):
{{{
#!sql
-- (for postgres)
ALTER TABLE my_table ALTER COLUMN my_column SET DEFAULT CURRENT_TIMESTAMP;
}}}
Now we save that as '''my_column-default.sql''' in the same directory as the generated '''schema.sql''' file (usually in projectdir/build/sql/).
== 2. Tell Propel Generator about the new file ==
In that same directory (where your '''schema.sql''' is located), there is a '''sqldb.map''' file which contains a mapping of SQL DDL files to the database that they should be executed against. After running the propel generator, you will probably have a single entry in that file that looks like:
{{{
schema.sql=your-db-name
}}}
We want to simply add the new file we created to this file (future builds will preserve anything you add to this file). When we're done, the file will look like this:
{{{
schema.sql=your-db-name
my_column-default.sql=your-db-name
}}}
Now when you execute the ''insert-sql'' Propel generator target, the '''my_column-default.sql''' file will be executed against the ''your-db-name'' database.

View file

@ -0,0 +1,47 @@
= Copying Persisted Objects =
Propel provides the {{{copy()}}} method to perform copies of mapped row in the database. Note that Propel does '''not''' override the {{{__clone()}}} method; this allows you to create local duplicates of objects that map to the same persisted database row (should you need to do this).
The {{{copy()}}} method by default performs shallow copies, meaning that any foreign key references will remain the same.
{{{
#!php
<?php
$a = new Author();
$a->setFirstName("Aldous");
$a->setLastName("Huxley");
$p = new Publisher();
$p->setName("Harper");
$b = new Book();
$b->setTitle("Brave New World");
$b->setPublisher($p);
$b->setAuthor($a);
$b->save(); // so that auto-increment IDs are created
$bcopy = $b->copy();
var_export($bcopy->getId() == $b->getId()); // FALSE
var_export($bcopy->getAuthorId() == $b->getAuthorId()); // TRUE
var_export($bcopy->getAuthor() === $b->getAuthor()); // TRUE
?>
}}}
== Deep Copies ==
By calling {{{copy()}}} with a {{{TRUE}}} parameter, Propel will create a deep copy of the object; this means that any related objects will also be copied.
To continue with example from above:
{{{
#!php
<?php
$bdeep = $b->copy(true);
var_export($bcopy->getId() == $b->getId()); // FALSE
var_export($bcopy->getAuthorId() == $b->getAuthorId()); // FALSE
var_export($bcopy->getAuthor() === $b->getAuthor()); // FALSE
?>
}}}

View file

@ -0,0 +1,153 @@
= Customizing Build =
It is possible to customize the Propel build process by overriding values in your propel __build.properties__ file. For maximum flexibility, you can even create your own Phing __build.xml__ file.
== Customizing the build.properties ==
The easiest way to customize your Propel build is to simply specify build properties in your project's __build.properties__ file.
=== Understanding Phing build properties ===
''Properties'' are essentially variables. These variables can be specified on the commandline or in ''properties files''.
For example, here's how a property might be specified on the commandline:
{{{
$> phing -Dpropertyname=value
}}}
More typically, properties are stored in files and loaded by Phing. For those not familiar with Java properties files, these files look like PHP INI files; the main difference is that values in properties files can be references to other properties (a feature that will probably exist in in INI files in PHP 5.1).
'''Importantly:''' properties, once loaded, are not overridden by properties with the same name unless explicitly told to do so. In the Propel build process, the order of precedence for property values is as follows:
1. Commandline properties
1. Project __build.properties__
1. Top-level __build.properties__
1. Top-level __default.properties__
This means, for example, that values specified in the project's __build.properties__ files will override those in the top-level __build.properties__ and __default.properties__ files.
=== Changing values ===
To get an idea of what you can modify in Propel, simply look through the __build.properties__ and __default.properties__ files.
''Note, however, that some of the current values exist for legacy reasons and will be cleaned up in Propel 1.1.''
==== New build output directories ====
This can easily be customized on a project-by-project basis. For example, here is a __build.properties__ file for the ''bookstore ''project that puts the generated classes in __/var/www/bookstore/classes__ and puts the generated SQL in __/var/www/bookstore/db/sql__:
{{{
propel.project = bookstore
propel.database = sqlite
propel.database.url = sqlite://localhost/./test/bookstore.db
propel.targetPackage = bookstore
# directories
prope.output.dir = /var/www/bookstore
propel.php.dir = ${propel.output.dir}/classes
propel.phpconf.dir = ${propel.output.dir}/conf
propel.sql.dir = ${propel.output.dir}/db/sql
}}}
The ''targetPackage'' property is also used in determining the path of the generated classes. In the example above, the __Book.php__ class will be located at __/var/www/bookstore/classes/bookstore/Book.php__. You can change this __bookstore__ subdir by altering the ''targetPackage'' property:
{{{
propel.targetPackage = propelom
}}}
Now the class will be located at __/var/www/bookstore/classes/propelom/Book.php__
''Note that you can override the targetPackage property by specifying a package="" attribute in the <database> tag or even the <table> tag of the schema.xml.''
== Creating a custom build.xml file ==
If you want to make more major changes to the way the build script works, you can setup your own Phing build script. This actually is not a very scary task, and once you've managed to create a Phing build script, you'll probably want to create build targets for other aspects of your project (e.g. running batch unit tests is now supported in Phing 2.1-CVS).
To start with, I suggest taking a look at the __build-propel.xml__ script (the build.xml script is just a wrapper script). Note, however, that the __build-propel.xml__ script does a lot & has a lot of complexity that is designed to make it easy to configure using properties (so, don't be scared).
Without going into too much detail about how Phing works, the important thing is that Phing build scripts XML and they are grouped into ''targets'' which are kinda like functions. The actual work of the scripts is performed by ''tasks'', which are PHP5 classes that extend the base Phing ''Task'' class and implement its abstract methods. Propel provides some Phing tasks that work with templates to create the object model.
=== Step 1: register the needed tasks ===
The Propel tasks must be registered so that Phing can find them. This is done using the ''<taskdef>'' tag. You can see this near the top of the __build-propel.xml__ file.
For example, here is how we register the ''propel-om'' task, which is the task that creates the PHP classes for your object model:
{{{
<taskdef
name="propel-om"
classname="propel.phing.PropelOMTask"/>
}}}
Simple enough. Phing will now associate the ''<propel-data-model>'' tag with the ''PropelOMTask'' class, which it expects to find at __propel/phing/PropelOMTask.php__ (on your ''include_path''). If Propel generator classes are not on your ''include_path'', you can specify that path in your ''<taskdef>'' tag:
{{{
<taskdef
name="propel-om"
classname="propel.phing.PropelOMTask"
classpath="/path/to/propel-generator/classes"/>
}}}
Or, for maximum re-usability, you can create a ''<path>'' object, and then reference it (this is the way __build-propel.xml__ does it):
{{{
<path id="propelclasses">
<pathelement dir="/path/to/propel-generator/classes"/>
</path>
<taskdef
name="propel-om"
classname="propel.phing.PropelOMTask"
classpathRef="propelclasses"/>
}}}
=== Step 2: invoking the new task ===
Now that the ''<propel-om>'' task has been registered with Phing, it can be invoked in your build file.
{{{
<propel-om
outputDirectory="/var/www/bookstore/classes"
targetDatabase="mysql"
targetPackage="bookstore"
templatePath="/path/to/propel-generator/templates"
targetPlatform="php5">
<schemafileset dir="/var/www/bookstore/db/model" includes="*schema.xml"/>
</propel-om>
}}}
In the example above, it's worth pointing out that the ''<propel-om>'' task can actually transform multiple __schema.xml__ files, which is why there is a ''<schemafileset>'' sub-element. Phing ''filesets'' are beyond the scope of this HOWTO, but hopefully the above example is obvious enough.
=== Step 3: putting it together into a build.xml file ===
Now that we've seen the essential elements of our custom build file, it's time to look at how to assemble them into a working whole:
{{{
<?xml version="1.0">
<project name="propel" default="om">
<!-- set properties we use later -->
<property name="propelgen.home" value="/path/to/propel-generator"/>
<property name="out.dir" value="/var/www/bookstore"/>
<!-- register task -->
<path id="propelclasses">
<pathelement dir="${propelgen.home}/classes"/>
</path>
<taskdef
name="propel-om"
classname="propel.phing.PropelOMTask"
classpathRef="propelclasses"/>
<!-- this [default] target performs the work -->
<target name="om" description="build propel om">
<propel-om
outputDirectory="${out.dir}/classes"
targetDatabase="mysql"
targetPackage="bookstore"
templatePath="${propelgen.home}/templates"
targetPlatform="php5">
<schemafileset dir="${out.dir}/db/model" includes="*schema.xml"/>
</propel-om>
</target>
</project>
}}}
If that build script was named __build.xml__ then it could be executed by simply running ''phing'' in the directory where it is located:
{{{
$> phing om
}}}
Actually, specifying the ''om'' target is not necessary since it is the default.
Refer to the __build-propel.xml__ file for examples of how to use the other Propel Phing tasks -- e.g. ''<propel-sql>'' for generating the DDL SQL, ''<propel-sql-exec>'' for inserting the SQL, etc.

View file

@ -0,0 +1,95 @@
= Working With Existing Databases =
The following topics are targeted for developers who already have a working database solution in place, but would like to use Propel to work with the data. For this case, Propel provides a number of command-line utilities helping with migrations of data and data structures.
== Working with Database Structures ==
Propel uses an abstract XML schema file to represent databases (the [wiki:Documentation/1.5/Schema schema]). Propel builds the SQL specific to a database based on this schema. Propel also provides a way to reverse-engineer the generic schema file based on database metadata.
=== Creating an XML Schema from a DB Structure ===
To generate a schema file, create a new directory for your project & specify the connection information in your `build.properties` file for that project. For example, to create a new project, `legacyapp`, follow these steps:
1. Create the `legacyapp` project directory anywhere on your filesystem:
{{{
> mkdir legacyapp
> cd legacyapp
}}}
1. Create a `build.properties` file in `legacyapp/` directory with the DB connection parameters for your existing database, e.g.:
{{{
propel.project = legacyapp
# The Propel driver to use for generating SQL, etc.
propel.database = mysql
# This must be a PDO DSN
propel.database.url = mysql:dbname=legacyapp
propel.database.user = root
# propel.database.password =
}}}
1. Run the `reverse` task to generate the `schema.xml`:
{{{
> propel-gen reverse
}}}
1. Pay attention to any errors/warnings issued by Phing during the task execution and then examine the generated `schema.xml` file to make any corrections needed.
1. '''You're done! ''' Now you have a `schema.xml` file in the `legacyapp/` project directory. You can now run the default Propel build to generate all the classes.
The generated `schema.xml` file should be used as a guide, not a final answer. There are some datatypes that Propel may not be familiar with; also some datatypes are simply not supported by Propel (e.g. arrays in PostgreSQL). Unfamiliar datatypes will be reported as warnings and substituted with a default VARCHAR datatype.
Tip: The reverse engineering classes may not be able to provide the same level of detail for all databases. In particular, metadata information for SQLite is often very basic since SQLite is a typeless database.
=== Migrating Structure to a New RDBMS ===
Because Propel has both the ability to create XML schema files based on existing database structures and to create RDBMS-specific DDL SQL from the XML schema file, you can use Propel to convert one database into another.
To do this you would simply:
1. Follow the steps above to create the `schema.xml` file from existing db.
1. Then you would change the target database type and specify connection URL for new database in the project's `build.properties` file:
{{{
propel.database = pgsql
propel.database.url = pgsql://unix+localhost/newlegacyapp
}}}
1. And then run the `sql` task to generate the new DDL:
{{{
> propel-gen sql
}}}
1. And (optionally) the `insert-sql` task to create the new database:
{{{
> propel-gen insert-sql
}}}
== Working with Database Data ==
Propel also provides several tasks to facilitate data import/export. The most important of these are `datadump` and `datasql`. The first dumps data to XML and the second converts the XML data dump to a ready-to-insert SQL file.
Tip: Both of these tasks require that you already have generated the `schema.xml` for your database.
=== Dumping Data to XML ===
Once you have created (or reverse-engineered) your `schema.xml` file, you can run the `datadump` task to dump data from the database into a `data.xml` file.
{{{
> propel-gen datadump
}}}
The task transfers database records to XML using a simple format, where each row is an element, and each column is an attribute. So for instance, the XML representation of a row in a `publisher` table:
||'''publisher_id'''||'''name'''||
||1||William Morrow||
... is rendered in the `data.xml` as follows:
{{{
<dataset name="all">
...
<Publisher PublisherId="1" Name="William Morrow"/>
...
</dataset>
}}}
=== Creating SQL from XML ===
To create the SQL files from the XML, run the `datasql` task:
{{{
> propel-gen datasql
}}}
The generated SQL is placed in the `build/sql/` directory and will be inserted when you run the `insert-sql` task.

View file

@ -0,0 +1,73 @@
= Working with LOB Columns =
Propel uses PHP streams internally for storing ''Binary'' Locator Objects (BLOBs). This choice was made because PDO itself uses streams as a convention when returning LOB columns in a resultset and when binding values to prepared statements. Unfortunately, not all PDO drivers support this (see, for example, http://bugs.php.net/bug.php?id=40913); in those cases, Propel creates a {{{php://temp}}} stream to hold the LOB contents and thus provide a consistent API.
Note that CLOB (''Character'' Locator Objects) are treated as strings in Propel, as there is no convention for them to be treated as streams by PDO.
== Getting BLOB Values ==
BLOB values will be returned as PHP stream resources from the accessor methods. Alternatively, if the value is NULL in the database, then the accessors will return the PHP value NULL.
{{{
#!php
<?php
$media = MediaPeer::retrieveByPK(1);
$fp = $media->getCoverImage();
if ($fp !== null) {
echo stream_get_contents($fp);
}
}}}
== Setting BLOB Values ==
When setting a BLOB column, you can either pass in a stream or the blob contents.
=== Setting using a stream ===
{{{
#!php
<?php
$fp = fopen("/path/to/file.ext", "rb");
$media = new Media();
$media->setCoverImage($fp);
}}}
=== Setting using file contents ===
{{{
#!php
<?php
$media = new Media();
$media->setCoverImage(file_get_contents("/path/to/file.ext"));
}}}
Regardless of which setting method you choose, the BLOB will always be represented internally as a stream resource -- ''and subsequent calls to the accessor methods will return a stream.''
For example:
{{{
#!php
<?php
$media = new Media();
$media->setCoverImage(file_get_contents("/path/to/file.ext"));
$fp = $media->getCoverImage();
print gettype($fp); // "resource"
}}}
=== Setting BLOB columns and isModified() ===
Note that because a stream contents may be externally modified, ''mutator methods for BLOB columns will always set the '''isModified()''' to report true'' -- even if the stream has the same identity as the stream that was returned.
For example:
{{{
#!php
<?php
$media = MediaPeer::retrieveByPK(1);
$fp = $media->getCoverImage();
$media->setCoverImage($fp);
var_export($media->isModified()); // TRUE
}}}

View file

@ -0,0 +1,94 @@
= Replication =
Propel can be used in a master-slave replication environment. These environments are set up to improve the performance of web applications by dispatching the database-load to multiple database-servers. While a single master database is responsible for all write-queries, multiple slave-databases handle the read-queries. The slaves are synchronised with the master by a fast binary log (depending on the database).
== Configuring Propel for Replication ==
* Set up a replication environment (see the Databases section below)
* Use the latest Propel-Version from SVN
* add a slaves-section to your {{{runtime-conf.xml}}} file
* verify the correct setup by checking the masters log file (should not contain "select ..." statements)
You can configure Propel to support replication by adding a <slaves> element with nested <connection> element(s) to your {{{runtime-conf.xml}}}.
The <slaves> section is at the same level as the master <connection> and contains multiple nested <connection> elements with the same information as the top-level (master) <connection>. It is recommended that they are numbered. The follwing example shows a slaves section with a several slave connections configured where "localhost" is the master and "slave-server1" and "slave-server2" are the slave-database connections.
{{{
#!xml
<?xml version="1.0"?>
<config>
<log>
<ident>propel-bookstore</ident>
<name>console</name>
<level>7</level>
</log>
<propel>
<datasources default="bookstore">
<datasource id="bookstore">
<adapter>sqlite</adapter>
<connection>
<dsn>mysql:host=localhost;dbname=bookstore</dsn>
<user>testuser</user>
<password>password</password>
</connection>
<slaves>
<connection>
<dsn>mysql:host=slave-server1; dbname=bookstore</dsn>
<user>testuser</user>
<password>password</password>
</connection>
<connection>
<dsn>mysql:host=slave-server2; dbname=bookstore</dsn>
<user>testuser</user>
<password>password</password>
</connection>
</slaves>
</datasource>
</datasources>
</propel>
</config>
}}}
== Implementation ==
The replication functionality is implemented in the Propel connection configuration and initialization code and in the generated Peer and Object classes.
=== Propel::getConnection() ===
When requesting a connection from Propel ('''Propel::getConnection()'''), you can either specify that you want a READ connection (slave) or WRITE connection (master). Methods that are designed to perform READ operations, like the '''doSelect*()''' methods of your generated Peer classes, will always request a READ connection like so:
{{{
#!php
<?php
$con = Propel::getConnection(MyPeer::DATABASE_NAME, Propel::CONNECTION_READ);
}}}
Other methods that are designed to perform write operations will explicitly request a Propel::CONNECTION_WRITE connection. The WRITE connections are also the default, however, so applications that make a call to '''Propel::getConnection()''' without specifying a connection mode will always get a master connection.
If you do have configured slave connections, Propel will choose a single random slave to use per request for any connections where the mode is Propel::CONNECTION_READ.
Both READ (slave) and WRITE (master) connections are only configured on demand. If all of your SQL statements are SELECT queries, Propel will never create a connection to the master database (unless, of course, you have configured Propel to always use the master connection -- see below).
'''Important:''' if you are using Propel to execute custom SQL queries in your application (and you want to make sure that Propel respects your replication setup), you will need to explicitly get the correct connection. For example:
{{{
#!php
<?php
$con = Propel::getConnection(MyPeer::DATABASE_NAME, Propel::CONNECTION_READ);
$stmt = $con->query('SELECT * FROM my');
/* ... */
}}}
=== Propel::setForceMasterConnection() ===
You can force Propel to always return a WRITE (master) connection from '''Propel::getConnection()''' by calling '''Propel::setForceMasterConnection(true);'''. This can be useful if you must be sure that you are getting the most up-to-date data (i.e. if there is some latency possible between master and slaves).
== Databases ==
=== MySql ===
http://dev.mysql.com/doc/refman/5.0/en/replication-howto.html
== References ==
* Henderson Carl (2006): Building Scalable Web Sites. The Flickr Way. O'Reilly. ISBN-596-10235-6.

View file

@ -0,0 +1,297 @@
= Multi-Component Data Model =
Propel comes along with packaging capabilities that allow you to more easily integrate Propel into a packaged or modularized application.
== Muliple Schemas ==
You can use as many `schema.xml` files as you want. Schema files have to be named `(*.)schema.xml`, so names like `schema.xml`, `package1.schema.xml`, `core.package1.schema.xml` are all acceptable. These files ''have'' to be located in your project directory.
Each schema file has to contain a `<database>` element with a `name` attribute. This name references the connection settings to be used for this database (and configured in the `runtime-conf.xml`), so separated schemas can share a common database name.
Whenever you call a propel build taks, Propel will consider all these schema files and build the classes (or the SQL) for all the tables.
== Understanding Packages ==
In Propel, a ''package'' represents a group of models. This is a convenient way to organize your code in a modularized way, since classes and SQL files of a given package are be grouped together and separated from the other packages. By carefully choosing the package of each model, applications end up in smaller, independent modules that are easier to manage.
=== Package Cascade ===
The package is defined in a configuration cascade. You can set it up for the whole project, for all the tables of a schema, or for a single table.
For the whole project, the main package is set in the `build.properties`:
{{{
#!ini
propel.targetPackage = my_project
}}}
By default, all the tables of all the schemas in the project use this package. However, you can override the package for a given `<database>` by setting its `package` attribute:
{{{
#!xml
<!-- in author.schema.xml -->
<database package="author" name="bookstore">
<table name="author">
<!-- author columns -->
</table>
</database>
<!-- in book.schema.xml -->
<database package="book" name="bookstore">
<table name="book">
<!-- book columns -->
</table>
<table name="review">
<!-- review columns -->
</table>
</database>
}}}
In this example, thanks to the `package` attribute, the tables are grouped into the following packages:
* `my_project.author` package: `author` table
* `my_project.book` package: `book` and `review` tables
'''Warning''': If you separate tables related by a foreign key into separate packages (like `book` and `author` in this example), you must enable the `packageObjectModel` build property to let Propel consider other packages for relations.
You can also override the `package` attribute at the `<table>` element level.
{{{
#!xml
<!-- in author.schema.xml -->
<database package="author" name="bookstore">
<table name="author">
<!-- author columns -->
</table>
</database>
<!-- in book.schema.xml -->
<database package="book" name="bookstore">
<table name="book">
<!-- book columns -->
</table>
<table name="review" package="review">
<!-- review columns -->
</table>
</database>
}}}
This ends up in the following package:
* `my_project.author` package: `author` table
* `my_project.book` package: `book` table
* `my_project.review` package: `review` table
Notice that tables can end up in separated packages even though they belong to the same schema file.
'''Tip''': You can use dots in a package name to add more package levels.
=== Packages and Generated Model Files ===
The `package` attribute of a table translates to the directory in which Propel generates the Model classes for this table.
For instance, if no `package` attribute is defined at the database of table level, Propel places all classes according to the `propel.targetPackage` from the `build.properties`:
{{{
build/
classes/
my_project/
om/
map/
Author.php
AuthorPeer.php
AuthorQuery.php
Book.php
BookPeer.php
BookQuery.php
Review.php
ReviewPeer.php
ReviewQuery.php
}}}
You can further tweak the location where Propel puts the created files by changing the `propel.output.dir` build property. By default this property is set to:
{{{
#!ini
propel.output.dir = ${propel.project.dir}/build
}}}
You can change it to use any other directory as your build directory.
If you set up packages for `<database>` elements, Propel splits up the generated model classes into subdirectories named after the package attribute:
{{{
build/
classes/
my_project/
author/
om/
map/
Author.php
AuthorPeer.php
AuthorQuery.php
book/
om/
map/
Book.php
BookPeer.php
BookQuery.php
Review.php
ReviewPeer.php
ReviewQuery.php
}}}
And of course, if you specialize the `package` attribute per table, you can have one table use its own package:
{{{
build/
classes/
my_project/
author/
om/
map/
Author.php
AuthorPeer.php
AuthorQuery.php
book/
om/
map/
Book.php
BookPeer.php
BookQuery.php
review/
om/
map/
Review.php
ReviewPeer.php
ReviewQuery.php
}}}
=== Packages And SQL Files ===
Propel also considers packages for SQL generation. In practice, Propel generates one SQL file per package. Each file contains the CREATE TABLE SQL statements necessary to create all the tables of a given package.
So by default, all the tables end up in a single SQL file:
{{{
build/
sql/
schema.sql
}}}
If you specialize the `package` for each `<database>` element, Propel uses it for SQL files:
{{{
build/
sql/
author.schema.sql // contains CREATE TABLE author
book.schema.sql // contains CREATE TABLE book and CREATE TABLE review
}}}
And, as you probably expect it, a package overridden at the table level also acocunts for an independent SQL file:
{{{
build/
sql/
author.schema.sql // contains CREATE TABLE author
book.schema.sql // contains CREATE TABLE book
review.schema.sql // contains CREATE TABLE review
}}}
== Understanding The packageObjectModel Build Property ==
The `propel.packageObjectModel` build property enables the "packaged" build process. This modifies the build tasks behavior by joining `<database>` elements of the same name - but keeping their packages separate. That allows to split a large schema into several files, regardless of foreign key dependencies, since Propel will join all schemas using the same database name.
To switch this on, simply add the following line to the `build.properties` file in your project directory:
{{{
propel.packageObjectModel = true
}}}
== The Bookstore Packaged Example ==
In the bookstore-packaged example you'll find the following schema files:
* author.schema.xml
* book.schema.xml
* club.schema.xml
* media.schema.xml
* publisher.schema.xml
* review.schema.xml
* log.schema.xml
Each schema file has to contain a `<database>` tag that has its `package` attribute set to the package name where ''all'' of the tables in this schema file/database belong to.
For example, in the bookstore-packaged example the `author.schema.xml` contains the following `<database>` tag:
{{{
<database package="core.author" name="bookstore" [...]>
}}}
That means, that the Author OM classes will be created in a subdirectory `core/author/` of the build output directory.
You can have more than one schema file that belong to one package. For example, in the the bookstore-packaged example both the `book.schema.xml` and `media.schema.xml` belong to the same package "core.book". The generated OM classes for these schemas will therefore end up in the same `core/book/` subdirectory.
=== The OM build ===
To run the packaged bookstore example build simply go to the `propel/test/fixtures/bookstore-packages/` directory and type:
{{{
../../../generator/bin/propel-gen om
}}}
This should run without any complaints. When you have a look at the projects/bookstore-packaged/build/classes directory, the following directory tree should have been created:
{{{
addon/
club/
BookClubList.php
BookClubListPeer.php
BookListRel.php
BookListRelPeer.php
core/
author/
Author.php
AuthorPeer.php
book/
Book.php
BookPeer.php
Media.php
MediaPeer.php
publisher/
Publisher.php
PublisherPeer.php
review/
Review.php
ReviewPeer.php
util/
log/
BookstoreLog.php
BookstoreLogPeer.php
}}}
(The additional subdirectories map/ and om/ in each of these directories have been omitted for clarity.)
== The SQL build ==
From the same schema files, run the SQL generation by calling:
{{{
../../../generator/bin/propel-gen sql
}}}
Then, have a look at the `build/sql/` directory: you will see that for each package (that is specified as a package attribute in the schema file database tags), one sql file has been created:
* addon.club.schema.sql
* core.author.schema.sql
* core.book.schema.sql
* core.publisher.schema.sql
* core.review.schema.sql
* util.log.schema.sql
These files contain the CREATE TABLE SQL statements necessary for each package.
When you now run the insert-sql task by typing:
{{{
../../../generator/bin/propel-gen insert-sql
}}}
these SQL statements will be executed on a SQLite database located in the Propel/generator/test/ directory.

View file

@ -0,0 +1,133 @@
= How to Use PHP 5.3 Namespaces =
The generated model classes can use a namespace. It eases the management of large database models, and makes the Propel model classes integrate with PHP 5.3 applications in a clean way.
== Namespace Declaration And Inheritance ==
To define a namespace for a model class, you just need to specify it in a `namespace` attribute of the `<table>` element for a single table, or in the `<database>` element to set the same namespace to all the tables.
Here is an example schema using namespaces:
{{{
#!xml
<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<database name="bookstore" defaultIdMethod="native" namespace="Bookstore">
<table name="book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
<column name="title" type="VARCHAR" required="true" primaryString="true" />
<column name="isbn" required="true" type="VARCHAR" size="24" phpName="ISBN" />
<column name="price" required="false" type="FLOAT" />
<column name="publisher_id" required="false" type="INTEGER" description="Foreign Key Publisher" />
<column name="author_id" required="false" type="INTEGER" description="Foreign Key Author" />
<foreign-key foreignTable="publisher" onDelete="setnull">
<reference local="publisher_id" foreign="id" />
</foreign-key>
<foreign-key foreignTable="author" onDelete="setnull" onUpdate="cascade">
<reference local="author_id" foreign="id" />
</foreign-key>
</table>
<table name="author">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER"/>
<column name="first_name" required="true" type="VARCHAR" size="128" />
<column name="last_name" required="true" type="VARCHAR" size="128" />
<column name="email" type="VARCHAR" size="128" />
</table>
<table name="publisher" namespace="Book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
<column name="name" required="true" type="VARCHAR" size="128" default="Penguin" />
</table>
<table name="user" namespace="\Admin">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER"/>
<column name="login" required="true" type="VARCHAR" size="128" />
<column name="email" type="VARCHAR" size="128" />
</table>
</database>
}}}
The `<database>` element defines a `namespace` attribute. The `book` and `author` tables inherit their namespace from the database, therefore the generated classes for these tables will be `\Bookstore\Book` and `\Bookstore\Author`.
The `publisher` table defines a `namespace` attribute on ots own, which ''extends'' the database namespace. That means that the generated class will be `\Bookstore\Book\Publisher`.
As for the `user` table, it defines an absolute namespace (starting with a backslash), which ''overrides'' the database namespace. The generated class for the `user` table will be `Admin\User`.
'''Tip''': You can use subnamespaces (i.e. namespaces containing backslashes) in the `namespace` attribute.
== Using Namespaced Models ==
Namespaced models benefit from the Propel runtime autoloading just like the other model classes. You just need to alias them, or to use their fully qualified name.
{{{
#!php
<?php
// use an alias
use Bookstore\Book;
$book = new Book();
// or use fully qualified name
$book = new \Bookstore\Book();
}}}
Relation names forged by Propel don't take the namespace into account. That means that related getter and setters make no mention of it:
{{{
#!php
<?php
$author = new \Bookstore\Author();
$book = new \Bookstore\Book();
$book->setAuthor($author);
$book->save();
}}}
The namespace is used for the ActiveRecord class, but also for the Query and Peer classes. Just remember that when you use relation names ina query, the namespace should not appear:
{{{
#!php
<?php
$author = \Bookstore\AuthorQuery::create()
->useBookQuery()
->filterByPrice(array('max' => 10))
->endUse()
->findOne();
}}}
Related tables can have different namespaces, it doesn't interfere with the functionality provided by the object model:
{{{
#!php
<?php
$book = \Bookstore\BookQuery::create()
->findOne();
echo get_class($book->getPublisher());
// \Bookstore\Book\Publisher
}}}
'''Tip''': Using namespaces make generated model code incompatible with versions of PHP less than 5.3. Beware that you will not be able to use your model classes in an older PHP application.
== Using Namespaces As A Directory Structure ==
In a schema, you can define a `package` attribute on a `<database>` or a `<table>` tag to generate model classes in a subdirectory (see [wiki:Documentation/1.5/Multi-Component]). If you use namespaces to autoload your classes based on a SplClassAutoloader (see http://groups.google.com/group/php-standards), then you may find yourself repeating the `namespace` data in the `package` attribute:
{{{
#!xml
<database name="bookstore" defaultIdMethod="native"
namespace="Foo/Bar" package="Foo.Bar">
}}}
To avoid such repetitions, just set the `propel.namespace.autoPackage` setting to `true` in your `build.properties`:
{{{
#!ini
propel.namespace.autoPackage = true
}}}
Now Propel will automatically create a `package` attribute, and therefore distribute model classes in subdirectories, based on the `namespace` attribute, and you can omit the manual `package` attribute in the schema:
{{{
#!xml
<database name="bookstore" defaultIdMethod="native" namespace="Foo/Bar">
}}}

View file

@ -0,0 +1,183 @@
= !NestedSet support in Propel =
'''Warning''': Since Propel 1.5, the support for nested sets was moved to the `nested_set` behavior. The method described here is deprecated.'''
== Description ==
With !NestedSet implementation, trees are stored using different approach in databases: [http://www.sitepoint.com/article/hierarchical-data-database]
Nested Set implementation requires three dedicated fields in table structure
* left
* right
Plus an optional fields for multi nested set support
* scope
''NB: fields name are free and must be defined in schema.xml''
To enable !NestedSet support in table, schema.xml must define some specific attributes:
'''treeMode''' which must take the value '''!NestedSet'''
{{{
#!xml
<table name="menu" idMethod="native" treeMode="NestedSet">
}}}
Then, left and right field must be defined that way '''nestedSetLeftKey''' as a boolean value as '''nestedSetRightKey'''
{{{
#!xml
<column name="lft" type="INTEGER" required="true" default="0" nestedSetLeftKey="true"/>
<column name="rgt" type="INTEGER" required="true" default="0" nestedSetRightKey="true"/>
}}}
For multi nestedset support, an other column must be defined with boolean attribute '''treeScopeKey''' set to true
{{{
#!xml
<column name="scope" type="INTEGER" required="true" default="0" treeScopeKey="true"/>
}}}
And then, let's the propel generator automagically create all the needed model and stub classes.
== !NestedSet usage in Propel ==
''ex:''
'''schema.xml''' extract
{{{
#!xml
<table name="menu" idMethod="native" treeMode="NestedSet">
<column name="id" type="INTEGER" required="true" autoIncrement="true" primaryKey="true"/>
<column name="lft" type="INTEGER" required="true" default="0" nestedSetLeftKey="true"/>
<column name="rgt" type="INTEGER" required="true" default="0" nestedSetRightKey="true"/>
<column name="scope" type="INTEGER" required="true" default="0" treeScopeKey="true"/>
<column name="text" type="VARCHAR" size="128" required="true" default=""/>
<column name="link" type="VARCHAR" size="255" required="true" default=""/>
<index name="lft">
<index-column name="lft"/>
</index>
<index name="rgt">
<index-column name="rgt"/>
</index>
<index name="scope">
<index-column name="scope"/>
</index>
</table>
}}}
=== !NestedSet insertion ===
{{{
#!php
<?php
$root = new Menu();
$root->setText('Google');
$root->setLink('http://www.google.com');
$root->makeRoot();
$root->save();
$menu = new Menu();
$menu->setText('Google Mail');
$menu->setLink('http://mail.google.com');
$menu->insertAsLastChildOf($root);
$menu->save();
$child = new Menu();
$child->setText('Google Maps');
$child->setLink('http://maps.google.com');
$child->insertAsLastChildOf($root);
$child->save();
$sibling = new Menu();
$sibling->setText('Yahoo!');
$sibling->setLink('http://www.yahoo.com');
$sibling->insertAsNextSiblingOf($root);
$sibling->save();
$child = new Menu();
$child->setText('Yahoo! Mail');
$child->setLink('http://mail.yahoo.com');
$child->insertAsLastChildOf($sibling);
$child->save();
}}}
=== Multi !NestedSet insertion ===
{{{
#!php
<?php
// Create first root node
$root = new Menu();
$root->setText('Google');
$root->setLink('http://www.google.com');
$root->makeRoot();
$root->setScopeIdValue(1); // Tree 1
$root->save();
$menu = new Menu();
$menu->setText('Google Mail');
$menu->setLink('http://mail.google.com');
$menu->insertAsLastChildOf($root);
$menu->save();
// Create secund root node
$root2 = new Menu();
$root2->setText('Yahoo!');
$root2->setLink('http://www.yahoo.com');
$root2->makeRoot();
$root2->setScopeIdValue(2); // Tree 2
$root2->save();
$menu = new Menu();
$menu->setText('Yahoo! Mail');
$menu->setLink('http://mail.yahoo.com');
$menu->insertAsLastChildOf($root2);
$menu->save();
}}}
=== Tree retrieval ===
{{{
#!php
<?php
class myMenuOutput extends RecursiveIteratorIterator {
function __construct(Menu $m) {
parent::__construct($m, self::SELF_FIRST);
}
function beginChildren() {
echo str_repeat("\t", $this->getDepth());
}
function endChildren() {
echo str_repeat("\t", $this->getDepth() - 1);
}
}
$menu = MenuPeer::retrieveTree($scopeId);
$it = new myMenuOutput($menu);
foreach($it as $m) {
echo $m->getText(), '[', $m->getLeftValue(), '-', $m->getRightValue(), "]\n";
}
}}}
=== Tree traversal ===
!NestetSet implementation use the [http://somabo.de/talks/200504_php_quebec_spl_for_the_masses.pdf SPL RecursiveIterator] as suggested by soenke
== !NestedSet known broken behaviour ==
=== Issue description ===
For every changes applied on the tree, several entries in the database can be involved. So all already loaded nodes have to be refreshed with their new left/right values.
=== InstancePool enabled ===
In order to refresh all loaded nodes, an automatic internal call is made after each tree change to retrieve all instance in InstancePool and update them.
And it works fine.
=== InstancePool disabled ===
When InstancePool is disabled, their is no way to retrieve references to all already loaded node and get them updated.
So in most case, all loaded nodes are not updated and it leads to an inconsistency state.
So, workaround is to do an explicit reload for any node you use after tree change.

View file

@ -0,0 +1,164 @@
= Model Introspection At Runtime =
In addition to the object and peer classes used to do C.R.U.D. operations, Propel generates an object mapping for your tables to allow runtime introspection.
The intospection objects are instances of the map classes. Propel maps databases, tables, columns, validators, and relations into objects that you can easily use.
== Retrieving a TableMap ==
The starting point for runtime introspection is usually a table map. This objects stores every possible property of a table, as defined in the `schema.xml`, but accessible at runtime.
To retrieve a table map for a table, use the `getTableMap()` static method of the related peer class. For instance, to retrieve the table map for the `book` table, just call:
{{{
#!php
<?php
$bookTable = BookPeer::getTableMap();
}}}
== TableMap properties ==
A `TableMap` object carries the same information as the schema. Check the following example to see how you can read the general properties of a table from its map:
{{{
#!php
<?php
echo $bookTable->getName(); // 'table'
echo $bookTable->getPhpName(); // 'Table'
echo $bookTable->getPackage(); // 'bookstore'
echo $bookTable->isUseIdGenerator(); // true
}}}
Tip: A TableMap object also references the `DatabaseMap` that contains it. From the database map, you can also retrieve other table maps using the table name or the table phpName:
{{{
#!php
<?php
$dbMap = $bookTable->getDatabaseMap();
$authorTable = $dbMap->getTable('author');
$authorTable = $dbMap->getTablebyPhpName('Author');
}}}
To introspect the columns of a table, use any of the `getColumns()`, `getPrimaryKeys()`, and `getForeignKeys()` `TableMap` methods. They all return an array of `ColumnMap` objects.
{{{
#!php
<?php
$bookColumns = $bookTable->getColumns();
foreach ($bookColumns as $column) {
echo $column->getName();
}
}}}
Alternatively, if you know a column name, you can retrieve the corresponding ColumnMap directly using the of `getColumn($name)` method.
{{{
#!php
<?php
$bookTitleColumn = $bookTable->getColumn('title');
}}}
The `DatabaseMap` object offers a shortcut to every `ColumnMap` object if you know the fully qualified column name:
{{{
#!php
<?php
$bookTitleColumn = $dbMap->getColumn('book.TITLE');
}}}
== ColumnMaps ==
A `ColumnMap` instance offers a lot of information about a table column. Check the following examples:
{{{
#!php
<?php
$bookTitleColumn->getTableName(); // 'book'
$bookTitleColumn->getTablePhpName(); // 'Book'
$bookTitleColumn->getType(); // 'VARCHAR'
$bookTitleColumn->getSize(); // 255
$bookTitleColumn->getDefaultValue(); // null
$bookTitleColumn->isLob(); // false
$bookTitleColumn->isTemporal(); // false
$bookTitleColumn->isEpochTemporal(); // false
$bookTitleColumn->isNumeric(); // false
$bookTitleColumn->isText(); // true
$bookTitleColumn->isPrimaryKey(); // false
$bookTitleColumn->isForeignKey(); // false
$bookTitleColumn->hasValidators(); // false
}}}
`ColumnMap` objects also keep a reference to their parent `TableMap` object:
{{{
#!php
<?php
$bookTable = $bookTitleColumn->getTable();
}}}
Foreign key columns give access to more information, including the related table and column:
{{{
#!php
<?php
$bookPublisherIdColumn = $bookTable->getColumn('publisher_id');
echo $bookPublisherIdColumn->isForeignKey(); // true
echo $bookPublisherIdColumn->getRelatedName(); // 'publisher.ID'
echo $bookPublisherIdColumn->getRelatedTableName(); // 'publisher'
echo $bookPublisherIdColumn->getRelatedColumnName(); // 'ID'
$publisherTable = $bookPublisherIdColumn->getRelatedTable();
$publisherRelation = $bookPublisherIdColumn->getRelation();
}}}
== RelationMaps ==
To get an insight on all the relationships of a table, including the ones relying on a foreign key located in another table, you must use the `RelationMap` objects related to a table.
If you know its name, you can retrieve a `RelationMap` object using `TableMap::getRelation($relationName)`. Note that the relation name is the phpName of the related table, unless the foreign key defines a phpName in the schema. For instance, the name of the `RelationMap` object related to the `book.PUBLISHER_ID` column is 'Publisher'.
{{{
#!php
<?php
$publisherRelation = $bookTable->getRelation('Publisher');
}}}
alternatively, you can access a `RelationMap` from a foreign key column using `ColumnMap::getRelation()`, as follows:
{{{
#!php
<?php
$publisherRelation = $bookTable->getColumn('publisher_id')->getRelation();
}}}
Once you have a `RelationMap` instance, inspect its properties using any of the following methods:
{{{
#!php
<?php
echo $publisherRelation->getType(); // RelationMap::MANY_TO_ONE
echo $publisherRelation->getOnDelete(); // 'SET NULL'
$bookTable = $publisherRelation->getLocalTable();
$publisherTable = $publisherRelation->getForeignTable();
print_r($publisherRelation->getColumnMappings());
// array('book.PUBLISHER_ID' => 'publisher.ID')
print_r(publisherRelation->getLocalColumns());
// array($bookPublisherIdColumn)
print_r(publisherRelation->getForeignColumns());
// array($publisherBookIdColumn)
}}}
This also works for relationships referencing the current table:
{{{
#!php
<?php
$reviewRelation = $bookTable->getRelation('Review');
echo $reviewRelation->getType(); // RelationMap::ONE_TO_MANY
echo $reviewRelation->getOnDelete(); // 'CASCADE'
$reviewTable = $reviewRelation->getLocalTable();
$bookTable = $reviewRelation->getForeignTable();
print_r($reviewRelation->getColumnMappings());
// array('review.BOOK_ID' => 'book.ID')
}}}
To retrieve all the relations of a table, call `TableMap::getRelations()`. You can then iterate over an array of `RelationMap` objects.
Tip: RelationMap objects are lazy-loaded, which means that the `TableMap` will not instanciate any relation object until you call `getRelations()`. This allows the `TableMap` to remain lightweight for when you don't use relationship introspection.

View file

@ -0,0 +1,424 @@
= How to Write A Behavior =
Behaviors are a good way to reuse code across models without requiring inheritance (a.k.a. horizontal reuse). This step-by-step tutorial explains how to port model code to a behavior, focusing on a simple example.
In the tutorial "[http://propel.posterous.com/getting-to-know-propel-15-keeping-an-aggregat Keeping an Aggregate Column up-to-date]", posted in the [http://propel.posterous.com/ Propel blog], the `TotalNbVotes` property of a `PollQuestion` object was updated each time a related `PollAnswer` object was saved, edited, or deleted. This "aggregate column" behavior was implemented by hand using hooks in the model classes. To make it truly reusable, the custom model code needs to be refactored and moved to a Behavior class.
== Boostrapping A Behavior ==
A behavior is a class that can alter the generated classes for a table of your model. It must only extend the [browser:branches/1.5/generator/lib/model/Behavior.php `Behavior`] class and implement special "hook" methods. Here is the class skeleton to start with for the `aggregate_column` behavior:
{{{
#!php
<?php
class AggregateColumnBehavior extends Behavior
{
// default parameters value
protected $parameters = array(
'name' => null,
);
}
}}}
Save this class in a file called `AggregateColumnBehavior.php`, and set the path for the class file in the project `build.properties` (just replace directory separators with dots). Remember that the `build.properties` paths are relative to the include path:
{{{
#!ini
propel.behavior.aggregate_column.class = path.to.AggregateColumnBehavior
}}}
Test the behavior by adding it to a table of your model, for instance to a `poll_question` table:
{{{
#!xml
<database name="poll" defaultIdMethod="native">
<table name="poll_question" phpName="PollQuestion">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
<column name="body" type="VARCHAR" size="100" />
<behavior name="aggregate_column">
<parameter name="name" value="total_nb_votes" />
</behavior>
</table>
</database>
}}}
Rebuild your model, and check the generated `PollQuestionTableMap` class under the `map` subdirectory of your build class directory. This class carries the structure metadata for the `PollQuestion` ActiveRecord class at runtime. The class should feature a `getBehaviors()` method as follows, proving that the behavior was correctly applied:
{{{
#!php
<?php
class PollQuestionTableMap extends TableMap
{
// ...
public function getBehaviors()
{
return array(
'aggregate_column' => array('name' => 'total_nb_votes', ),
);
} // getBehaviors()
}
}}}
== Adding A Column ==
The behavior works, but it still does nothing at all. Let's make it useful by allowing it to add a column. In the `AggregateColumnBehavior` class, just implement the `modifyTable()` method with the following code:
{{{
#!php
<?php
class AggregateColumnBehavior extends Behavior
{
// ...
public function modifyTable()
{
$table = $this->getTable();
if (!$columnName = $this->getParameter('name')) {
throw new InvalidArgumentException(sprintf(
'You must define a \'name\' parameter for the \'aggregate_column\' behavior in the \'%s\' table',
$table->getName()
));
}
// add the aggregate column if not present
if(!$table->containsColumn($columnName)) {
$table->addColumn(array(
'name' => $columnName,
'type' => 'INTEGER',
));
}
}
}
}}}
This method shows that a behavior class has access to the `<parameters>` defined for it in the `schema.xml` through the `getParameter()` command. Behaviors can also always access the `Table` object attached to them, by calling `getTable()`. A `Table` can check if a column exists and add a new one easily. The `Table` class is one of the numerous generator classes that serve to describe the object model at buildtime, together with `Column`, `ForeignKey`, `Index`, and a lot more classes. You can find all the buildtime model classes under the [browser:branches/1.5/generator/lib/model generator/lib/model] directory.
'''Tip''': Don't mix up the ''runtime'' database model (`DatabaseMap`, `TableMap`, `ColumnMap`, `ValidatorMap`, `RelationMap`) with the ''buildtime'' database model (`Database`, `Table`, `Column`, `Validator`, etc.). The buildtime model is very detailed, in order to ease the work of the builders that write the ActiveRecord and Query classes. On the other hand, the runtime model is optimized for speed, and carries minimal information to allow correct hydration and binding at runtime. Behaviors use the buildtime object model, because they are run at buildtime, so they have access to the most powerful model.
Now rebuild the model and the SQL, and sure enough, the new column is there. `BasePollQuestion` offers a `getTotalNbVotes()` and a `setTotalNbVotes()` method, and the table creation SQL now includes the additional `total_nb_votes` column:
{{{
#!sql
DROP TABLE IF EXISTS poll_question;
CREATE TABLE poll_question
(
id INTEGER NOT NULL AUTO_INCREMENT,
title VARCHAR(100),
total_nb_votes INTEGER,
PRIMARY KEY (id)
)Type=InnoDB;
}}}
'''Tip''': The behavior only adds the column if it's not present (`!$table->containsColumn($columnName)`). So if a user needs to customize the column type, or any other attribute, he can include a `<column>` tag in the table with the same name as defined in the behavior, and the `modifyTable()` will then skip the column addition.
== Adding A Method To The ActiveRecord Class ==
In the previous post, a method of the ActiveRecord class was in charge of updating the `total_nb_votes` column. A behavior can easily add such methods by implementing the `objectMethods()` method:
{{{
#!php
<?php
class AggregateColumnBehavior extends Behavior
{
// ...
public function objectMethods()
{
$script = '';
$script .= $this->addUpdateAggregateColumn();
return $script;
}
protected function addUpdateAggregateColumn()
{
$sql = sprintf('SELECT %s FROM %s WHERE %s = ?',
$this->getParameter('expression'),
$this->getParameter('foreign_table'),
$this->getParameter('foreign_column')
);
$table = $this->getTable();
$aggregateColumn = $table->getColumn($this->getParameter('name'));
$columnPhpName = $aggregateColumn->getPhpName();
$localColumn = $table->getColumn($this->getParameter('local_column'));
return "
/**
* Updates the aggregate column {$aggregateColumn->getName()}
*
* @param PropelPDO \$con A connection object
*/
public function update{$columnPhpName}(PropelPDO \$con)
{
\$sql = '{$sql}';
\$stmt = \$con->prepare(\$sql);
\$stmt->execute(array(\$this->get{$localColumn->getPhpName()}()));
\$this->set{$columnPhpName}(\$stmt->fetchColumn());
\$this->save(\$con);
}
";
}
}
}}}
The ActiveRecord class builder expects a string in return to the call to `Behavior::objectMethods()`, and appends this string to the generated code of the ActiveRecord class. Don't bother about indentation: builder classes know how to properly indent a string returned by a behavior. A good rule of thumb is to create one behavior method for each added method, to provide better readability.
Of course, the schema must be modified to supply the necessary parameters to the behavior:
{{{
#!xml
<database name="poll" defaultIdMethod="native">
<table name="poll_question" phpName="PollQuestion">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
<column name="body" type="VARCHAR" size="100" />
<behavior name="aggregate_column">
<parameter name="name" value="total_nb_votes" />
<parameter name="expression" value="count(nb_votes)" />
<parameter name="foreign_table" value="poll_answer" />
<parameter name="foreign_column" value="question_id" />
<parameter name="local_column" value="id" />
</behavior>
</table>
<table name="poll_answer" phpName="PollAnswer">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
<column name="question_id" required="true" type="INTEGER" />
<column name="body" type="VARCHAR" size="100" />
<column name="nb_votes" type="INTEGER" />
<foreign-key foreignTable="poll_question" onDelete="cascade">
<reference local="question_id" foreign="id" />
</foreign-key>
</table>
</database>
}}}
Now if you rebuild the model, you will see the new `updateTotalNbVotes()` method in the generated `BasePollQuestion` class:
{{{
#!php
<?php
class BasePollQuestion extends BaseObject
{
// ...
/**
* Updates the aggregate column total_nb_votes
*
* @param PropelPDO $con A connection object
*/
public function updateTotalNbVotes(PropelPDO $con)
{
$sql = 'SELECT count(nb_votes) FROM poll_answer WHERE question_id = ?';
$stmt = $con->prepare($sql);
$stmt->execute(array($this->getId()));
$this->setTotalNbVotes($stmt->fetchColumn());
$this->save($con);
}
}
}}}
Behaviors offer similar hook methods to allow the addition of methods to the query classes (`queryMethods()`) and to the peer classes (`peerMethods()`). And if you need to add attributes, just implement one of the `objectAttributes()`, `queryAttributes()`, or `peerAttributes()` methods.
== Using a Template For Generated Code ==
The behavior's `addUpdateAggregateColumn()` method is somehow hard to read, because of the large string containing the PHP code canvas for the added method. Propel behaviors can take advantage of Propel's simple templating system to use an external file as template for the code to insert.
Let's refactor the `addUpdateAggregateColumn()` method to take advantage of this feature:
{{{
#!php
<?php
class AggregateColumnBehavior extends Behavior
{
// ...
protected function addUpdateAggregateColumn()
{
$sql = sprintf('SELECT %s FROM %s WHERE %s = ?',
$this->getParameter('expression'),
$this->getParameter('foreign_table'),
$this->getParameter('foreign_column')
);
$table = $this->getTable();
$aggregateColumn = $table->getColumn($this->getParameter('name'));
return $this->renderTemplate('objectUpdateAggregate', array(
'aggregateColumn' => $aggregateColumn,
'columnPhpName' => $aggregateColumn->getPhpName(),
'localColumn' => $table->getColumn($this->getParameter('local_column')),
'sql' => $sql,
));
}
}
}}}
The method no longer returns a string created by hand, but a ''rendered template''. Propel templates are simple PHP files executed in a sandbox - they have only access to the variables declared as second argument of the `renderTemplate()` call.
Now create a `templates/` directory in the same directory as the `AggregateColumnBehavior` class file, and add in a `objectUpdateAggregate.php` file with the following code:
{{{
#!php
/**
* Updates the aggregate column <?php echo $aggregateColumn->getName() ?>
*
* @param PropelPDO $con A connection object
*/
public function update<?php echo $columnPhpName ?>(PropelPDO $con)
{
$sql = '<?php echo $sql ?>';
$stmt = $con->prepare($sql);
$stmt->execute(array($this->get<?php echo $localColumn->getPhpName() ?>()));
$this->set<?php echo $columnPhpName ?>($stmt->fetchColumn());
$this->save($con);
}
}}}
No need to escape dollar signs anymore: this syntax allows for a cleaner separation, and is very convenient for large behaviors.
== Adding Another Behavior From A Behavior ==
This is where it's getting tricky. In the [http://propel.posterous.com/getting-to-know-propel-15-keeping-an-aggregat blog post] describing the column aggregation technique, the calls to the `updateTotalNbVotes()` method come from the `postSave()` and `postDelete()` hooks of the `PollAnswer` class. But the current behavior is applied to the `poll_question` table, how can it modify the code of a class based on another table?
The short answer is: it can't. To modify the classes built for the `poll_answer` table, a behavior must be registered on the `poll_answer` table. But a behavior is just like a column or a foreign key: it has an object counterpart in the buildtime database model. So the trick here is to modify the `AggregateColumnBehavior::modifyTable()` method to ''add a new behavior'' to the foreign table. This second behavior will be in charge of implementing the `postSave()` and `postDelete()` hooks of the `PollAnswer` class.
{{{
#!php
<?php
class AggregateColumnBehavior extends Behavior
{
// ...
public function modifyTable()
{
// ...
// add a behavior to the foreign table to autoupdate the aggregate column
$foreignTable = $table->getDatabase()->getTable($this->getParameter('foreign_table'));
if (!$foreignTable->hasBehavior('concrete_inheritance_parent')) {
require_once 'AggregateColumnRelationBehavior.php';
$relationBehavior = new AggregateColumnRelationBehavior();
$relationBehavior->setName('aggregate_column_relation');
$relationBehavior->addParameter(array(
'name' => 'foreign_table',
'value' => $table->getName()
));
$relationBehavior->addParameter(array(
'name' => 'foreign_column',
'value' => $this->getParameter('name')
));
$foreignTable->addBehavior($relationBehavior);
}
}
}
}}}
In practice, everything now happens as if the `poll_answer` had its own behavior:
{{{
#!xml
<database name="poll" defaultIdMethod="native">
<!-- ... -->
<table name="poll_answer" phpName="PollAnswer">
<!-- ... -->
<behavior name="aggregate_column_relation">
<parameter name="foreign_table" value="poll_question" />
<parameter name="foreign_column" value="total_nb_votes" />
</behavior>
</table>
</database>
}}}
Adding a behavior to a `Table` instance, as well as adding a `Parameter` to a `Behavior` instance, is quite straightforward. And since the second behavior class file is required in the `modifyTable()` method, there is no need to add a path for it in the `build.properties`.
== Adding Code For Model Hooks ==
The new `AggregateColumnRelationBehavior` is yet to write. It must implement a call to `PollQuestion::updateTotalNbVotes()` in the `postSave()` and `postDelete()` hooks.
Adding code to hooks from a behavior is just like adding methods: add a method with the right hook name returning a code string, and the code will get appended at the right place. Unsurprisingly, the behavior hook methods for `postSave()` and `postDelete()` are called `postSave()` and `postDelete()`:
{{{
#!php
<?php
class AggregateColumnBehavior extends Behavior
{
// default parameters value
protected $parameters = array(
'foreign_table' => null,
'foreignColumn' => null,
);
public function postSave()
{
$table = $this->getTable();
$foreignTable = $table->getDatabase()->getTable($this->getParameter('foreign_table'));
$foreignColumn = $foreignTable->getColumn($this->getParameter('foreign_column'));
$foreignColumnPhpName = $foreignColumn->getPhpName();
return "\$this->updateRelated{$foreignColumnPhpName}(\$con)";
}
public function postDelete()
{
return $this->postSave();
}
public function objectMethods()
{
$script = '';
$script .= $this->addUpdateRelatedAggregateColumn();
return $script;
}
protected function addUpdateRelatedAggregateColumn()
{
$table = $this->getTable();
$foreignTable = $table->getDatabase()->getTable($this->getParameter('foreign_table'));
$foreignTablePhpName = foreignTable->getPhpName();
$foreignColumn = $foreignTable->getColumn($this->getParameter('foreign_column'));
$foreignColumnPhpName = $foreignColumn->getPhpName();
return "
/**
* Updates an aggregate column in the foreign {$foreignTable->getName()} table
*
* @param PropelPDO \$con A connection object
*/
protected function updateRelated{$foreignColumnPhpName}(PropelPDO \$con)
{
if (\$parent{$foreignTablePhpName} = \$this->get{$foreignTablePhpName}()) {
\$parent{$foreignTablePhpName}->update{$foreignColumnPhpName}(\$con);
}
}
";
}
}
}}}
The `postSave()` and `postDelete()` behavior hooks will not add code to the ActiveRecord `postSave()` and `postDelete()` methods - to allow users to further implement these methods - but instead it adds code directly to the `save()` and `delete()` methods, inside a transaction. Check the generated `BasePollAnswer` class for the added code in these methods:
{{{
#!php
<?php
// aggregate_column_relation behavior
$this->updateRelatedTotalNbVotes($con);
}}}
You will also see the new `updateRelatedTotalNbVotes()` method added by `AggregateColumnBehavior::objectMethods()`:
{{{
#!php
<?php
/**
* Updates an aggregate column in the foreign poll_question table
*
* @param PropelPDO $con A connection object
*/
protected function updateRelatedTotalNbVotes(PropelPDO $con)
{
if ($parentPollQuestion = $this->getPollQuestion()) {
$parentPollQuestion->updateTotalNbVotes($con);
}
}
}}}
== What's Left ==
These are the basics of behavior writing: implement one of the methods documented in the [wiki:Documentation/1.5/Behaviors#WritingaBehavior behaviors chapter] of the Propel guide, and return strings containing the code to be added to the ActiveRecord, Query, and Peer classes. In addition to the behavior code, you should always write unit tests - all the behaviors bundled with Propel have full unit test coverage. And to make your behavior usable by others, documentation is highly recommended. Once again, Propel core behaviors are fully documented, to let users understand the behavior usage without having to peek into the code.
As for the `AggregateColumnBehavior`, the job is not finished. The [http://propel.posterous.com/getting-to-know-propel-15-keeping-an-aggregat blog post] emphasized the need for hooks in the Query class, and these are not yet implemented in the above code. Besides, the post kept quiet about one use case that left the aggregate column not up to date (when a question is detached from a poll without deleting it). Lastly, the parameters required for this behavior are currently a bit verbose, especially concerning the need to define the foreign table and the foreign key - this could be simplified thanks to the knowledge of the object model that behaviors have.
All this is left to the reader as an exercise. Fortunately, the final behavior is part of the Propel core behaviors, so the [browser:branches/1.5/generator/lib/behavior/aggregate_column code], [browser:branches/1.5/test/testsuite/generator/behavior/aggregate_column unit tests], and [wiki:Documentation/1.5/Behaviors/aggregate_column documentation] are all ready to help you to further understand the power of Propel's behavior system.