Has_and_belongs_to_many (habtm for short) is handy, it even creates a join model and installer by itself. But what if you want to have extra columns in the join table with information on the relation between the two tables?
Not possible in habtm, and there is no has_many :through like in Rails, but there is another way!
Let's say we have the string instruments shop from the habtm example. Because sales aren't going so well the owner decided to rent his instruments to clients. So, if you your violin keeps sounding awful, even after years of practice, you could rent that Stradivarius! For a weekend or so.
Apart from the products table, we need two more: one with the clients and one with the rentals.
Generate the models:
script/generate model product script/generate model client script/generate model rental
And we could combine the installers into one:
<?php class PcInstaller extends AkInstaller { function up_1() { $this->createTable('products', " id, name varchar(100), description varchar(1000), selling_price numeric(12,2), rental_price_per_day numeric(6,2) "); $this->createTable('clients', " id, first_name varchar(100), last_name varchar(100), address varchar(1000), telephone numeric(12,2) "); $this->createTable('rentals', " id, product_id, client_id, pickup_date date, return_date date "); } } ?>
So:
A habtm would suffice, were it not that we keep extra information in the join table: pickup_date and return_date, enabling the owner to keep track of when instrument should return to the shop. Plus, a day count between the two dates would enable a calculation of the total rental price for an instrument.
After some scaffolding we can define the relationships between the models.
class Product extends ActiveRecord { var $has_many = array('rentals' => array('dependent' => 'destroy')); }
class Client extends ActiveRecord { var $has_many = array('rentals' => array('dependent' => 'destroy')); }
class Rental extends ActiveRecord { var $belongs_to = array('product','client'); }
(The dependent ⇒ destroy option makes sure associated rentals are deleted together with a client or product.)
Now, in both the product_controller and to client_controller you should of course load the associated models.
In product_controller:
var $models = array('product','rental');
In client_controller:
var $models = array('client','rental');
Now we can load the rentals in the client controller using:
$client->rental->load();
..when showing or editing a single client record.
(for a listing we would include array('include' ⇒ 'rental') as option in the finder)
And in a view we can loop through the rental columns using:
<ul>
{loop client.rentals}
<li>Rented an instrument from {rental.pickup_date} till {rental.return_date}</li>
{end}
</ul>
.. but which instruments did the client take home?
Well, we'll have to make a special finder for that in the model.
In the client model we first make a method that can load the product details for each rental:
function loadProductForEachRental() { foreach ($this->Rentals as &$Rental){ $Rental->Product->load(); } }
And then we can make a custom finder which makes use of method above:
function findClientWithRentalsAndBelongingProducts($id) { $Client = $this->find($id,array('include'=>'rental')); $Client->loadProductForEachRental(); return $Client; }
So in the edit or show function in the client_controller we can now call:
$this->Client = $this-> Client->findClientWithRentalsAndBelongingProducts(@$this->params['id']);
Which will make this possible in the view:
<ul>
{loop client.rentals}
<li>Rented {rental.product.name} for {rental.product.rental_price_per_day} a day from {rental.pickup_date} till {rental.return_date}</li>
{end}
</ul>