Jan ten Bokkel Mon Apr 04 04:28:07 -0400 2011

Subject: Set relationship

Hi, I was wondering how to nicely set a relationship in PHP ActiveRecord.

For example I have a class "book" with a one to many relationship to "pages". For every page I'd have to write:
$page->book_id = $book->id;

Wouldn't it be great to do something like:
$page->book = $book;
And thereby doing the same thing as the previous example. Currently it would throw an error because the attribute "book" is unknown to "page".

When I write:
$page->book_id = $book;
An error will be thrown because the object can't be saved in the mysql database, obviously.


Jan ten Bokkel Mon Apr 04 06:07:38 -0400 2011

I created an addition for the __set function in ActiveRecord\Model:

 1 // If a relationship is assigned an object, translate it to a reference id
 2 $table = static::table();
 3 if(($table->has_relationship($name)) && ($class = get_class($value))) {
 4     $relation = $table->get_relationship($name);
 5     if($class == $relation->class_name) {
 6         return $this->assign_attribute($relation->foreign_key[0], $value->id);
 7     }
 8     else {
 9         throw new RelationshipException();
10     }
11 }

It allows you to set a relation like:

1 $book = new Book(); // has many pages
2 $page = new Page(); // belongs to book
3 $page->book = $book;

A RelationshipException will be thrown if there is a mismatch between the association type and the object type.

Yoan B Wed Apr 06 15:52:38 -0400 2011

Hi,

I did something similar.

https://github.com/kla/php-activerecord/issues#issue/125

I should improve mine by using `assign_attribute` and I think yours can be better not assuming that `id` is the primary key or that this object has already been saved.

You or I should start a branch with the proper tests with that, as it's a must have imho.

Jan ten Bokkel Wed Apr 06 17:11:18 -0400 2011

Hi Yoan,

First of all, nice to see that we share this idea and there are a lot of similarities between our pieces of code.

About the primary key, in the current nightly build the attribute 'id' is always mapped to the primary key in the __set function of the class Model:

1 if ($name == 'id')
2     return $this->assign_attribute($this->get_primary_key(true),$value);

But off course your method of looking up the primary key doesn't rely on that so it is better practice.

You bring up a good point about the primary key not existing yet when the assigned object has not been saved before. We should work on a solution for that. A simple one would be to save it before assigning but I'm sure we don't want a framework to do such thing.

Combining our code would result in something like:

 1 // If a relationship is assigned an object, translate it to a reference id
 2 $table = static::table();
 3 if(($table->has_relationship($name)) && ($class = get_class($value))) {
 4     $relation = $table->get_relationship($name);
 5     if($class == $relation->class_name) {
 6         if (!$value->is_new_record()) {
 7             $this->__relationships[$name] = $value;
 8             $pk = $value->get_primary_key(0);
 9             return $this->assign_attribute($relation->foreign_key[0], $value->$pk);
10         }
11         else {
12             /* @TODO: Do something when primary key doesn't exist yet! */
13         }
14     }
15     else {
16         // The assigned object does not match this relationship
17         throw new RelationshipException();
18     }
19 }

Yoan B Sun Apr 10 14:19:30 -0400 2011

I put this into a branch: https://github.com/greut/php-activerecord/compare/gh125-setter-for-relations

Feel free to write more tests I you find a bias into what I did. Thanks for your input on this.

Jan ten Bokkel Sun Apr 10 14:36:39 -0400 2011

Hi Yoan,

Thanks for putting this into a branch, I have never used Github so it would take me some time to figure it out.

About the behavior when the model has no primary key yet: I think it it better to throw an exception instead of just skipping the operation because the programmer should be informed about this. Otherwise the programmer expects the association to be set but it is not.

Yoan B Sun Apr 10 15:00:32 -0400 2011

Actually I'm using this for a while and really want to have non-saved relations. Especially when building a form of linked elements (either creation or edition).

You can handle this on save/update time.

Yoan B Tue Apr 12 05:05:37 -0400 2011

example, into my tests where I don't care about saving them (at this point):

1 $item = new InvoiceItem(array(
2     'vat' => new Vat(array(
3         'rate' => 10
4     )),
5     'quantity' => 1,
6     'unit_price' => 1100
7 ));
Jan ten Bokkel Tue Apr 12 05:18:14 -0400 2011

But you can't save your example, right?

In the ideal situation the id would be resolved on save time but I don't how to implement that.
And when there are two objects related to eachother then we have what we call in Dutch the chicken-egg problem (which one to save first?)

Jan ten Bokkel Tue Apr 12 06:56:29 -0400 2011

I see that you updated the branch, there are some nice changes that prevent errors and improve performance but I don't know why you used $value->{$pk[0]} to get the primary key of the assigned object instead of $value->$pk[0].

And you can get rid of the "$class_name" variable because you only use it once.

Jan ten Bokkel Tue Apr 12 07:25:48 -0400 2011

I've extended the save function to scan for unsaved relations on save time:

 1 public function save($validate=true)
 2 {
 3     $this->verify_not_readonly('save');
 4 
 5     // Check if all relationship attributes are set
 6     foreach($this->__relationships as $relation => $entry) {
 7         if($entry->is_new_record()) {
 8             // Entry has no primary key so it can't be saved
 9             throw new RelationshipException();
10         }
11         else if($this->read_attribute($relation) !== $entry->get_primary_key(1)) {
12             // Entry has a primary key but the relationship attribute needs to be updated
13             $foreign_key = static::table()->get_relationship($relation)->foreign_key[0];
14             $this->assign_attribute($foreign_key, $entry->read_attribute($entry->get_primary_key(1))); 
15         }
16     }
17 
18     return $this->is_new_record() ? $this->insert($validate) : $this->update($validate);
19 }
If the relation has been saved then the primary key will be reassigned. So now we are able to write:
 1 $item = new InvoiceItem(array(
 2     'vat' => new Vat(array(
 3         'rate' => 10
 4     )),
 5     'quantity' => 1,
 6     'unit_price' => 1100
 7 ));
 8 
 9 $item->vat->save();
10 $item->save();

Edit: found some bugs while testing it, code updated.

Yoan B Tue Apr 12 13:25:07 -0400 2011

Jan → wrote:

I don't know why you used $value->{$pk[0]} to get the primary key of the assigned object instead of $value->$pk[0].

$value->$pk[0] seemed to me like $value->{$pk}[0] but it's obviously what I was meaning in first place, thanks.

And you can get rid of the "$class_name" variable because you only use it once.

I make a better usage of instanceof so you can assign a subclass of the model you expected too.

https://github.com/greut/php-activerecord/commit/cef0caab156d76bb3ae677c9680c750a1ae8b90e

Now I'll dive into this cool save thingy you just made.

Yoan B Tue Apr 12 13:33:35 -0400 2011

Why saving an item won't magically create the relations, calling $entry->save($validate) instead? So you're not forgetting about it… right.

A maybe related issue: https://github.com/kla/php-activerecord/issues/110

Jan ten Bokkel Tue Apr 12 13:56:17 -0400 2011

Yoan, I'm not sure if I understand you correctly but my extension is not a solution to issue #110 yet.

If you want to solve issue #110 by first saving the related objects, then you will start an endless loop in the following example:

 1 class Person extends ActiveRecord\Model {
 2     static $belongs_to = array(
 3         array('friend', 'class_name' => 'Person')
 4     );
 5 
 6     $jim = new Person();
 7     $amy = new Person();
 8     $amy->friend = $jim;
 9     $jim->friend = $amy;
10 
11     $jim->save(); // Endless loop!
12 }
This is what I called the chicken-egg problem a few posts back, Jim is trying to save his relation with Amy who tries to save her relation with Jim and he will try to save his relation with Amy ... until you decide to kill the php process.

I don't know the solution yet but there should be a way to tackle this problem, don't you think?

Yoan B Tue Apr 12 14:58:14 -0400 2011

A relational DB with constraints on will not let you do that either.

Jan ten Bokkel Tue Apr 12 17:13:15 -0400 2011

Do you have an example configuration of a database that will refuse making the cross relation? This is a part of mysql that I have not discovered yet.

Yoan B Wed Apr 13 01:50:56 -0400 2011

only if friend_id is not null but it doesn't make sense in this case, you're right.

Jan ten Bokkel Wed Apr 13 10:42:58 -0400 2011

I made a lot of progress on the recursive saving today.

 1 class Person extends ActiveRecord\Model {
 2     static $belongs_to = array(
 3         array('person')
 4     );
 5     static $has_many = array(
 6         array('people')
 7     );
 8 }
 9 
10 $jim = new Person(array('name' => 'Jim'));
11 $bert = new Person(array('name' => 'Bert'));
12 $amy = new Person(array('name' => 'Amy'));
13 
14 $jim->person = $amy;
15 $bert->person = $amy;
16 $amy->person = $jim;
17 $amy->save();
18 
19 echo $amy->people[0]->name; // This is Jim
20 echo $amy->people[1]->name; // This is Bert

This example will save Jim, Bert and Amy with one single save instruction and all relations will be set as well.

The other feature I've been working on is automatically updating the has_many or has_one relation on the referenced object. Amy knows that she is related to Jim and Bert even before saving any of the objects.

The code is in this branch: https://github.com/jbtbnl/php-activerecord/compare/master

Yoan B Wed Apr 13 10:45:36 -0400 2011
Jan ten Bokkel Wed Apr 13 11:44:22 -0400 2011

There is an issue with this example:

 1 class Person extends ActiveRecord\Model {
 2     static $belongs_to = array(
 3         array('person')
 4     );
 5     static $has_many = array(
 6         array('people')
 7     );
 8 }
 9 
10 $jim = new Person(array('name' => 'Jim'));
11 $bert = new Person(array('name' => 'Bert'));
12 $amy = new Person(array('name' => 'Amy'));
13 
14 $jim->person = $amy;
15 $jim->person = $bert;
16 
17 echo $amy->people[0]->name; // This is Jim, but the relation doesn't exist any more

Yoan B Sun Apr 17 06:45:12 -0400 2011

It doesn't set the opposite relation, which can be very problematic.

1 $blog = Blog::find(1); // this a blog with many posts
2 $post = new Post(array('blog' => $blog));
3 
4 $post->blog // that's easy
5 $blog->posts // that will have to load “all” the posts. not a good idea.

btw, I have this branch that make the $has_many relations lazy (just take a look at the tests): https://github.com/greut/php-activerecord/compare/gh49-arrayobject-relations

Jan ten Bokkel Sun Apr 17 10:42:16 -0400 2011

It's not that is doesn't set the opposite relation, it does.
The problem is that the relation isn't unset but I know how to fix it.

I like your lazy has_many implementation, thanks for showing me.
I'm writing kind of the same with the implements \ArrayAccess, \Countable, \IteratorAggregate stuff to be able to set relations through the has_many relation like:

1 $book->pages[] = $page1;
2 $book->pages[] = $page2;
It should be able to integrate with your lazy has_many implementation.

(1-21/21)