[Solar-talk] The new model class discussion, some questions
Paul M Jones
pmjones at ciaweb.net
Mon Feb 19 11:46:36 PST 2007
Hi Andreas,
>> I can see why you would say that. Now that I've explained that the
>> model represents the table and its relationships, and thus all
>> possible records, does it make more sense? Or would the example make
>> more sense if $nodes was called, e.g., $model_nodes? I'd like to
>> make the examples more clear if possible.
>
> Yes and no - any relationship of a table can be expressed as an
> relationship of a record, but not the other way around necessarily
> (that is, any relationship of a record could be expressed as an
> relationship of a table, which is impossible -- the context is wrong).
I fail to see why not; the record has its basis in a table
somewhere. In what particular case would it *not* be possible?
> The exact relationships of one record to other records morph and
> change depending on the current context (the current record). For
> example, consider the phrase "record n in table m references record x
> in table y" which makes sense for some record n. For tables you can,
> for example, only say generalizations like "table m references table
> y", but there isn't (or shouldn't be) any relationship expressable on
> the table-level (static) that couldn't be expressable on the
> record-level (instance).
I disagree here. You don't say that "table m references table y",
you say that "local column foo references foreign column
bar.foo_id". That is exactly the same as saying the record
references some other record. Add single-table inheritance to that
and you've got all the morphing you could want, and all at the table
level.
>>> Again I am pointing to DB_DataObject as one example of a true and
>>> working active record implementation. Another elegant example is
>>> phpdoctrine (which has gained some peculiar popularity in
>>> #solarphp at freenode lately :) ).
>>
>> Technically, DB_DataObject is not an ActiveRecord either. It too is
>> a combination of TableModule, ActiveRecord, DataMapper, etc.
>>
>> The thing that bothers be about DBDO, the CakePHP model class, and
>> some other PHP model classes is that they try to represent *both* the
>> table *and* the record in one class. It doesn't make sense to me;
>> you end up with finders that return instances of themselves, which in
>> turn have finders that can also return instances of themselves. For
>> example ...
>>
>> $dbdo = new DB_DataObject();
>> $record1 = $dbdo->find(1);
>> $record2 = $record1->find(2);
>>
>> So is DBDO a record, or a recordset, or a table? It's all of them at
>> the same time, which bothers me.
>
> This is a point where we are clearly divergent ;) The class represents
> the table, and the instance represents a record.
My understanding that the object instance still can access the table
directly, or re-find new records from the table. Am I incorrect in
that understanding?
> Yes, the record suddenly turns into a swiss army knife
> because of the shift of context for the find() et.al, but the
> methodology is very capable.
It is this shift in context that makes me unhappy. :-( To me, the
object should represent the same class of thing all the time, not
switch what it represents depending on context.
>>> Continuing with doctrine, this is quoted from:
>>> http://lists.solarphp.com/pipermail/solar-talk/2007-February/
>>> 002368.html
>>>> ... looks very similar to the intended use of Solar_Sql_Model.
>>>>
>>>> Doctrine usage:
>>>>
>>>> $table = $conn->getTable("User");
>>>>
>>>> // find by primary key
>>>> $user = $table->find(2);
>>>> if($user !== false)
>>>> print $user->name;
>>>>
>>>> // get all users
>>>> foreach($table->findAll() as $user) {
>>>> print $user->name;
>>>> }
>>>>
>>>> Solar_Sql_Model usage:
>>>>
>>>> $users = Solar::factory('Solar_Model_Users');
>>>>
>>>> // find by primary key
>>>> $user = $users->fetch(2);
>>>> if ($user->id) {
>>>> print $user->name;
>>>> }
>>>>
>>>> // get all users
>>>> foreach ($users->fetchAll() as $user) {
>>>> print $user->name;
>>>> }
>>>
>>>
>>> It looks similar, but those two pieces of code are doing two very
>>> different operations (yikes again).
>>
>> I don't think so at all: the Doctrine example uses a table instance
>> to return a record instance, and the Solar example uses a table
>> instance to return a record instance. What do you see as
>> dramatically different here?
>>
>>
>
> Types! :) Doctrine returns instances of User, while Solar returns
> instances of something that is definently not Solar_Model_Users. But
> that's not obvious from the code. Solar will probably return an
> instance of Solar_Sql_Model_Record.
>
> First of all, it's not intuitive to me. There is only one mention of a
> class -- Solar_Model_Users. But the result is of an entirely different
> type.
Well, sure it is -- and part of that carries over to single-table
inheritance, even in Rails ActiveRecord. If you had a R-AR "Nodes"
class, with a wiki sub-type, you could do something like this:
wiki = Nodes.find(:first, :cond => {'type' => 'wiki'});
The "wiki" object is of the Wikis model, and no class type was
specified.
>
>
>>> The doctrine example returns instances of User.
>>
>> It returns a record from the model, the same way the Solar one
>> returns a record from the model.
>>
>>
>
> It returns an instance of a model (User), whereas Solar returns a
> record from a different model (Users), which is not obvious and not
> the same thing.
It returns a record tied to the Model you queried. What am I missing
here?
>>> A User is a model whose instance maps directly to one record, and
>>> is extended from Doctrine_Record. The solar example returns
>>> instances of who-knows-what.
>>
>> I see *Users* as the model, and it returns an instance of user
>> records. In the same way, the Solar example returns a
>> Solar_Sql_Model_Record tied back to the original Model class; that
>> is, instances of an individual record from the model. It seems like
>> the same thing to me. Am I missing something here?
>>
>>
>
> Users may be a model, but intuitively it should be a composite model
> consisting of some set of User model instances.
It is the composite of all possible User instances, because it
represents the table and its relationships.
Again, what am I missing here?
>>> It might be Tipos_Sql_Model_Record_Blog.
>>> Or Solar_Sql_Model_Record. Who could possibly tell without
>>> peeking at
>>> the class stack?
>>
>> You don't need to look at the class stack at all; you can check (1)
>> the class type, or (2) look at the $model_class property to see what
>> model it's tied back to.
>>
>
> That only works in run-time :) Which is sort of the point -- the type
> is not obvious by looking at the source code.
I don't see how it could be much more clear: $user = $users->fetch
(1). We just fetched a record from the $users model.
> The model class represents the table and its relations (if any).
>
> One instance of the model class represents a) one specific record, or
> b) one new record ready to be save()'d, including the records
> relations to other records.
That would be true if we had late static binding, but we don't.
*Something* has to represent the table and its relationships, and
since we can't use statics effectively, we need an instance of the
model that can find records for us.
> Quick pseudo-example, showing contexts and a tiny bit of business
> logic as well as usage:
>
> class Human extends Model {
> ...
>
> // Update some (or all) humans, add some klingon as enemy
> // The static keyword makes all the difference here
> protected static function setEnemy(Klingon $enemy) {
> ...
> // Get a list of humans
> ...
> foreach($humans as $human) {
> $human->setEnemy($enemy);
> }
> }
The problem with the static here is that it will not necessarily be
inherited properly by subclasses, and it won't necessarily refer to
the right class. You seem to assume (correct me if I'm wrong) a
static $humans variable. However, using self::$humans will not limit
itself in subclassed Humans, that is, all subclasses will refer to
the top-level Humans::$humans list. This is because of the lack of
late static binding.
> // Update this record with a new enemy, notify observers
> protected function setEnemy(Klingon $enemy) {
> $this->enemy_id = $enemy->id;
> $this->save();
> $this->setChanged();
> $this->notifyObservers(); // Somebody help us crush the
> klingon!
> }
> }
To me, this logic would go at the table level, in a post-save()
method of some sort. You save, and then notify; clearly it's post-
save behavior that you want every time the enemy is changed. I may
be missing your point though. Do we have a real-world example?
> // And an example of usage:
> $worf = new Klingon();
> $worf->name = 'Lieutenant Worf';
> $worf->save();
> Human::setEnemy($worf);
As opposed to:
$klingons = Solar::factory('Klingons');
$worf = $klingons->fetchNew();
$worf->name = "Lt Worf";
$worf->save();
$humans = Solar::factory('Humans');
$humans->setEnemy($worf);
(The $humans and $klingons values are actually singletons, to mimic
their static nature and the fact that they refer to tables. Maybe
that's the source of the divergent opinion?)
> $picard = Human::get(1000); // Get the record with id 1000 (Jean-
> Luc Picard!)
> // Factory an instance of
> Human, populate it with
> // values from the record,
> and return it
>
> echo $picard->getEnemy()->name; // => 'Lieutenant Worf'
Now, in this particular case, I kind of see your point. But it seems
like a contrived example; when would you need *all* records to refer
to exactly the same value in such a way?
> The point here is that there is no distinction between a record and a
> model instance (domain object). Table relations (if any) are handled
> in static context, as illustrated above. I can almost pretend I'm
> hacking Java :) I'm sure the pros with reusability and decoupling etc
> are obvious.
>
>
>> Again, I see this as a problem born from PHP's lack of late-static
>> binding, and thus its inability to conveniently use a single class to
>> represent both the table (via static methods and properties) and its
>> records (via instance methods and properties). As such, we need two
>> objects: one for the table, and one for its records.
>>
>
> Can't it be hacked? Compromised? Simulated? Add dependency on PHP6? :)
Can't be hacked or simulated as things are now; I've tried various
approaches when I was trying to do inheritable configuration, and
failed each time. The only solution is an engine-level patch, and
I'm told by core PHP devs that it is so difficult as to be nearly
impossible without an engine rewrite. :-(
>> Now, we can get around the late static binding problem by explicitly
>> setting every necessary static property by hand, but then you have a
>> lot more work every time you set up a new Model class. It becomes
>> very difficult to extend from a base Model class and have that
>> extension figure out its own name, what table it should relate to,
>> etc.
>>
>> I hope this helps to clarify the approach I'm taking, and why; please
>> let me know if it does not, or if there's something I'm missing
>> here. It's entirely possible that I'm over-thinking this or ignoring
>> something obvious.
>
> The big problem I have with your approach is the dumb records.
"How dumb are they?" ;-) They're as smart as their related models
are, I think, since they tie back to the model for save() behavior, etc.
> If you
> keep the current implementation, then at least make it
> possible/easy/sane to subclass the Record class so I can add my
> precious domain logic and reuse them other places in my application :)
It should be easy already to subclass the Record class; the Model (as
it is now) will find the right one for you automatically. For example:
class Andreas_Model_Foo extends Solar_Sql_Model {
...
}
class Andreas_Model_Foo_Record extends Solar_Sql_Model_Record {
...
}
When you call $foo->fetch(1), the record you get back will be of the
Andreas_Model_Foo_Record class.
Having said all this, let me summaryize:
1. I very much dislike the context-switching used in DB_DO, et. al.
2. However, I see your point about record-based vs table-based, esp.
with respect to keeping logic in one place, and it's worth exploring.
As such, I might attempt an experimental revamp of the experimental
Solar_Sql_Model/Record/RecordSet classes to see if I can come up with
something closer to what you're talking about, as it seems like
Rodrigo et al are also interested in that mode of working. Can't
promise anything though. Before I do so: how many of you really
think the record-based approach is a better idea than table based for
most (80% or more) of common real-world cases?
--
Paul M. Jones <http://paul-m-jones.com>
Solar: Simple Object Library and Application Repository
for PHP5. <http://solarphp.com>
Join the Solar community wiki! <http://solarphp.org>
Savant: The simple, elegant, and powerful solution for
templates in PHP. <http://phpsavant.com>
More information about the solar-talk
mailing list