Getting data into your models
Querying data isn't really Ornament's job, we think. Say what? Well, the 'M' in ORM stands for 'mapper' after all - its job is to map data to your models, not to actually retrieve it. (Otherwise they'd have called it "Object Mapper-Getter", which admittedly would have made for a cute acronym.)
Having said that, however, adapters do need to implement a load
method. You
can utilise that to facilitate a form of autoloading.
<?php
class SimpleModel
{
use Ornament\Model;
public $id;
public $foo;
public function __construct(PDO $pdo)
{
$this->addAdapter(new Ornament\Adapter\Pdo($pdo));
}
}
$model = new SimpleModel($pdo);
$model->id = 1;
$model->load();
Querying a model's adapter
The above obviously only works on single models, and only if you happen to know the primary key.
Models can implement the Ornament\Query
trait. This gives them access to more
powerful querying utilities:
<?php
use Ornament\Pdo;
use Ornament\Query;
class SimpleModel
{
use Pdo;
use Query;
// ...etc...
}
To load a single model, use the static find
method:
<?php
$model = SimpleModel::find(['foo' => 'bar']);
The first argument to find
is some type of identifier for the model we want to
retrieve. For the PDO adapter, it's a hash of key/value pairs used to construct
the WHERE
parts of the query. The above example would evaluate to
where foo = 'bar'
. Other adapters might support more (or less) powerful
options you can pass in. E.g., an adapter for an API might just expect a single
value (the id
you need to pass to the endpoint).
We agree that conceptually it would be cleaner to use a different class for retrieving models, cfg. Doctrine's Repositories. However, this would also mean duplicating all kinds of logic and that's a bad tradeoff. By using a Trait, we can simply reuse the adapter logic already defined on the model.
Loading an array of models (a Collection)
The Query trait also provides a static query
method which returns an array of
models matching the $where
parameter:
<?php
$list = SimpleModel::query(['foo' => 'bar']);
$list
in this example is actually an Ornament\Collection
object. Collections
behave like arrays, but contain additional functionaliy (e.g. isDirty
on the
entire collection).
Custom collections
Often (especially on more complex projects) you'll want to predefine an
interface or query
methods. For instance, for a UserModel
you might need to
query active users (by your own definition), logged in users, bad users and
premium users. Remembering the exact $where
clause is cumbersoms and besides
not very DRY. Your models should simply define their own static methods passing
the correct parameters to self::query
:
<?php
class SimpleModel
{
// etc.
public static function queryActive()
{
return self::query(['active' => true]);
}
}
Note that you in no way need to forward to self::query
; as long as your
custom method returns a Collection
(just instantiate with an array of models
as a constructor argument) you're good to go.
Doctrine-like repositories
You can also go out of your way and place all query
-related methods in a
separate class, and still benefit from the setup already done:
<?php
abstract class SimpleService extends SimpleModel
{
// We move this here:
use Ornament\Query;
// Static query methods etc.
}
Ornament will see that the SimpleService
is abstract
and extends a
SimpleModel
, and know to use the latter when constructing models. This way,
for complex models with lots of query
types, you can abstract them away into
the Service class and keep the model itself clean.
Auto-loading relationships
Often you'll want to automagically load related objects, e.g. when an
ItemModel
has an owner
propery that is actually a UserModel
. For this,
your models can use the Autoload
trait and you should annotate your properties
accordingly:
<?php
class ItemModel
{
use Ornament\Pdo;
use Ornament\Autoload;
/**
* @Model UserModel
* @Mapping id = owner
* @Constructor [ pdo ]
*/
public $owner;
// etc
}
The default @Mapping
is "map autoloaded fieldname to id on the new model",
which normally makes sense. You only have to explicitly specify this if your
mapping is different (which it shouldn't be for 99% of the cases).
The @Constructor
annotation lets you pass properties on $this
(the calling
model) to the constructor of the child model. This is useful for single-adapter
projects; if your project grows and starts mixing adapters, you should consider
dependency injection for this and use
argument-less constructors.
Auto-loading one-to-many relationships
The reverse can also happen: for each UserModel
, you want all ItemModel
s
she "owns". This is similar; you only need to set the field's default value to
an empty array:
<?php
class UserModel
{
use Ornament\Pdo;
use Ornament\Autoload;
/**
* @Model ItemModel
* @Mapping owner = id
* @Constructor [ pdo ]
*/
public $items = [];
// etc
}
Ornament is smart enough to prevent infinite loops, but one should still take care when autoloading many relationships; the number of queries done can quickly grow out of control.
Manual loading
Of course, you don't need to use this; in fact, optimized queries are often too complicated to wrap in such an abstraction (the above is just quick and dirty for simpler models). Or you might be using something like Doctrine's repositories for this.
Simply write your own load
and query
implementations. load
should update
the current model with data as read from source, whilst query
should return
an array populated with instances of __CLASS__
according to the specified
parameters/options.
The function signature for query
is:
public function query($where, array $opts = [], array $ctor = []);
$opts
is a hash of adapter-specific options. For instance, the default Pdo
adapter supports limit(int)
, offset(int)
and order(string)
.
$ctor
is an optional array of constructor arguments your implementation should
use when instantiating the new models in the list.
In the function body, simply do whatever you need to do to get your data, and
then loop through it creating a new instance of __CLASS__
with the correct
properties set.
There's also a find
method which is shorthand for query(...)[0]
and takes
the same arguments.
You can also avoid using query
and find
alltogether; keep in mind though
that internally Ornament will still be using them, so for consistency it's
usually best to override them.