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.
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.
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 }
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.
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.
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.
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 ));
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?)
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.
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.
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.
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
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?
A relational DB with constraints on will not let you do that either.
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.
only if friend_id is not null
but it doesn't make sense in this case, you're right.
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
Very clever. for the record: https://github.com/jbtbnl/php-activerecord/compare/master
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
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
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)
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.