Eloquent inside Codeigniter
Ever the diplomat, I’ve been doing my best to use components of the wonderful Laravel
in CodeIgniter apps that I’ve been working on for clients. Whilst I’ve
switched almost completely to using Laravel as of late, I still work on
legacy CI apps (and still work with clients who use CI for new apps, for
that matter.) At any rate, Laravel is designed in a modular fashion, allowing me to plug my favourite components into CodeIgniter and use them without too much friction.
The most important of these components is, of course, the wonderful Eloquent ORM, which has provided me with as much joy as ActiveRecord did in the land of Ruby. Thankfully, getting Eloquent running inside CodeIgniter was easy. I’ll assume you have Composer installed – if you don’t, you really should. You’ll need to add Laravel’s database component to your composer.json (which includes Eloquent):
The class works like this: accepting an array of models and the name of the association, pull out all the related types and their IDs and then fetch each related object. After we’ve got the related objects, it’s just a matter of setting them as related objects, as if Eloquent has populated them itself.
The code is simple:
The most important of these components is, of course, the wonderful Eloquent ORM, which has provided me with as much joy as ActiveRecord did in the land of Ruby. Thankfully, getting Eloquent running inside CodeIgniter was easy. I’ll assume you have Composer installed – if you don’t, you really should. You’ll need to add Laravel’s database component to your composer.json (which includes Eloquent):
{
"require": {
"illuminate/database": "4.2.6"
}
}
I also use Composer’s autoloader as a broad replacement for CodeIgniter’s CI_Loader
, so I’ve added in some folders into my classmap. You don’t need to do this, so don’t worry too much. I use application/classes
as a dump-spot for random objects I need to use before I find a more
suitable place for them, so don’t worry too much about that, either:{
"autoload": {
"classmap": [
"application/classes",
"application/core",
"application/models",
"application/presenters",
"application/libraries"
]
}
}
Install the dependencies and generate the autoload files:$ composer install
You’ll also need to require the autoloader in order to access anything – I do this right at the top of my index.php file:require_once '../vendor/autoload.php';
I keep the Laravel database component setup code inside my config/database.php
config file. Not a particularly good idea, I know, so feel free to move
it somewhere more appropriate (perhaps a hook? or potentially a
library?). We need to call on the autoloader to give us access to the
database component:use Illuminate\Database\Capsule\Manager as Capsule;
We then create a new instance of the component and add a new connection:$capsule = new Capsule;
$capsule->addConnection(array(
'driver' => 'mysql',
'host' => $db['default']['hostname'],
'database' => $db['default']['database'],
'username' => $db['default']['username'],
'password' => $db['default']['password'],
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => $db['default']['dbprefix'],
));
We want this connection to be used as Eloquent’s connection, so we
should set it globally. While we’re there, let’s boot Eloquent as well:$capsule->setAsGlobal();
$capsule->bootEloquent();
We can now use Eloquent by using the namespace in our model files and
extending our models from the class. After that, everything works just
as it does in Laravel:use \Illuminate\Database\Eloquent\Model as Eloquent;
class User extends Eloquent { }
$users = User::all();
One thing that irked me about this setup was the fact that my
database queries didn’t show up in my profiler. The CI profiler is an
old but trusty tool for debugging and figuring out why pages are loading
slowly, or how best to optimise your application and reduce duplicated
queries. Back in our config/database.php, under the call to bootEloquent()
, we will grab an instance of Laravel’s event dispatcher:$events = new Illuminate\Events\Dispatcher;
We can now listen to the illuminate.query
event, which
is triggered every time a query is run. Inside the callback, we want to
grab the query text and insert the bindings into the text for display
(for more information about bindings, read here):$events->listen('illuminate.query', function($query, $bindings, $time, $name)
{
// Format binding data for sql insertion
foreach ($bindings as $i => $binding)
{
if ($binding instanceof \DateTime)
{
$bindings[$i] = $binding->format('\'Y-m-d H:i:s\'');
}
else if (is_string($binding))
{
$bindings[$i] = "'$binding'";
}
}
// Insert bindings into query
$query = str_replace(array('%', '?'), array('%%', '%s'), $query);
$query = vsprintf($query, $bindings);
Now we’ve got a displayable query (and, incidentally, a runnable
query, which is useful for debugging), we can add it to the profiler.
The profiler looks to the CI database library object for the query (and
its associated running time). Since CI_DB_driver
uses the var
keyword, we can access it publicly and shove the query onto the end.
CodeIgniter’s shitty design may not be particularly robust, but damn it
is flexible: // Add it into CodeIgniter
$db =& get_instance()->db;
$db->query_times[] = $time;
$db->queries[] = $query;
});
Now we have our listener, we just need to tell the DB component to notify our dispatcher of any events that may occur:$capsule->setEventDispatcher($events);
And violà, we have a query log.Eager-loading Eloquent polymorphic associations
Laravel’s Eloquent doesn’t allow for eager loading of polymorphic relationships. While this is a very niche issue, it has come up more than once now and I know that I’ll see it again. Rather than just put up with it and face a lot of redundant queries, I decided to write a small class to handle the fetching of polymorphic associations.The class works like this: accepting an array of models and the name of the association, pull out all the related types and their IDs and then fetch each related object. After we’ve got the related objects, it’s just a matter of setting them as related objects, as if Eloquent has populated them itself.
The code is simple:
class AssocFetcher
{
public function __construct($assoc)
{
$this->assoc = $assoc;
}
public function fetch($models)
{
$relationships = array();
$objects = array();
$typeCol = "{$this->assoc}_type";
$idCol = "{$this->assoc}_type";
foreach ($models as $model)
$relationships[$model->$typeCol][] = $model->$idCol;
foreach ($relationships as $class => $ids)
$objects[$class] = $class::findMany($ids);
foreach ($models as $model)
$model->setRelation($this->assoc, $objects[$model->$typeCol]->find($model->$idCol));
return $models;
}
}
I can then use it in my controller like so:$assocs = new AssocFetcher('subject');
$changes = $assocs->fetch(Change::all());
With the above code, each of my Change
objects will be populated with a subject
relationship, based on the polymorphic subject_type
and subject_id
columns. This way, I can use my polymorphic related object with ease:foreach ($changes as $change)
echo $change->subject->name;
With this class, I’ve gone down from O(1 + n) efficiency (1 query to get the changes, n queries to get each change’s subject) to O(1 + a), where a is the number of different types
of associated object. It can be the difference between 4 queries and
400, so it’s well worth investigating ways of reducing the query count
on the page by being a bit cleverer and a bit more forward-thinking.
Comments
Post a Comment