Developer Blog

  • Blog
  • /
  • Understanding ORM Wrappers
By Dracony on 15 August 2015

There seems to be some confusion about the wrapper approach to extending ORM models, so let’s take a closer look at it. The usual approach in ActiveRecord style ORMs is to extend some kind of a base model class and add your functionality to it, this tightly couples the ORM logic with your application logic, and makes the resulting class hard to test. Such coupling is usually solved by the Decorator pattern which is the one used in this case. Let’s take a look at a simplified example:

Imagine we have a typical ActiveRecord model that allows us to get an item by its id field and a User class extending it:

1
2
3
4
5
6
7
8
9
10
11
12
class Model
{
    public function getById($id)
    {
        //
    }
}

class User extends Model
{

}

Now we would like to add a special method to the User class that checks if the user id belongs to an administrator, depending on some field from the database, something like this:

1
2
3
4
5
6
7
8
class User extends Model
{
    public function isAdmin($id)
    {
        $user = $this->getById($id);
        return $user->role === 'administrator';
    }
}

This way the isAdmin method is relying on the database logic contained in the Model class, and even though the actual check we do is trivial, testing such a method is becoming slightly more complicated already.

Another problem to consider is that all models must extend the base Model class. Imagine the following setup:

1
2
3
4
5
6
7
8
9
class Item
{
    //some logic here
}

class Product extends Item
{

}

Now what if you would like to save Products using the ORM, but you don’t want to use ORM for Items that are not Products? Since Product needs to extend the Model class to be usable with ORM you will have to make Item extend it to in order for this to work. Doctrine solves this problem by getting rid of the base class, and moving the whole persistence layer outside.

What if the Model and User classes need different constructor parameters? You’ll have too keep track of those in your User class too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Model
{
    // some dependencies
    public function __construct($database, $orm)
    {
        $this->database = $database;
        $this->orm = $orm;
    }
}

class User
{
    public function __construct($database, $orm, $auth)
    {
        parent::__construct($database, $orm);
        $this->auth = $auth;
    }
}

Solving this all with wrappers

Fortunately some OOP can help us solve all of these issues.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// first we add a Model interface
interface ModelInterface
{
    public function getById($id);
}

// and make the model implement it
class Model implements ModelInterface
{
    public function __construct($database, $orm)
    {
        $this->database = $database;
        $this->orm = $orm;
    }

    public function getById($id)
    {
        //
    }
}

// A wrapper also implements the interface.
// It acts as a proxy passing all method calls
// to an actual Model passd in its constructor
class ModelWrapper implements ModelInterface
{
    protected $model;

    public function __construct(ModelInterface $model)
    {
        $this->model = $model;
    }

    public function getById($id)
    {
        //method calls are proxied
        //to the underlying model
        return $this->model->getById($id);
    }
}

// The User class can now extend the wrapper instead
class User extends ModelWrapper
{
    // We now only need the actual
    // user realted dependencies
    // and the model to proxy calls to
    public function __construct(ModelInterface $model, $auth)
    {
        parent::__construct($model);
        $this->auth = $auth;
    }

    // This method doesn't need to change
    // since the getById() is proxied
    public function isAdmin($id)
    {
        $user = $this->getById($id);
        return $user->role === 'administrator';
    }
}

// We would instantiate it like this:
$model = new Model($database, $orm);
$user = new User($model, $auth);

This might look somewhat more complex to the usual approach, but the code the end user cares about, the actual User class did not suffer much modifications. But it did get a lot of flexibility. We can now easily test it by supplying a mocked Model instance as a parameter, this way the code behind the User class is no longer tied to anything related to the database allowing us test it as an any other simple class.

1
$user = new User($mockedModel, $auth);

We can now also resolve the issue of having the Item and Product classes by adding proxy methods to the Prodcut class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Item
{
    //some logic here
}

// This still required copy pasting
// proxy methods, but unlike pasting
// methods directly from the Model class
// we don't have to worry about updating
// the Product class when the Model class changes
class Product extends Item implements ModelInterface
{
    protected $model;

    public function __construct(ModelInterface $model)
    {
        $this->model = $model;
    }

    public function getById($id)
    {
        return $this->model->getById($id);
    }
}

The main reason why such an approach was taken with the PHPixie ORM is that Entities, Repositories and Queries require constructor dependencies that user classes don’t really need to know about. Using wrappers allows to bring the actual instantiation to the user (the ORMWrappers class in your project) while still hiding those parameters. So if at some point the ORM component gets refactored and the implementation changes, your wrappers should still work perfectly well with it.

comments powered by Disqus