Implementation Details

Obviously a developer can dig through the code to see exactly how things work, but as that's a more extreme measure most people probably don't bother with (and honestly ADM is kinda complicated), here's the general understanding of how ADM is implemented. We'll start by talking about the ActiveRecord and Data-Mapper patterns as those are an important segway into understanding the implementation of ADM.

ORM Basics

Alright, so first off, Object Relational Mapping only makes sense to use when you are doing object oriented programming. If you aren't familiar with OO programming, I'd recommend you look into it, as it makes keeping your app well organized way easier. Assuming however that you have a basic understanding of what objects are, let's discuss what an ORM does.

Say you have a simple User object that looks like this:

class User
{
    public $name;
    public $email;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }
}

Because HTTP is a stateless protocol, each time someone makes a new request to your webapp, the application has to be reinitialized from scratch. This means any data that's needed for your app to run must be fetched from various sources when setting up, and must be saved back to a permanent storage space after use (or else the data will simply be thrown out and lost). Aside from data you only need to keep temporarily (Google 'php sessions tutorial'), anything you want to store permanently you'll probably be saving to a database. If you've worked with objects in the past outside of a framework, your solution to saving an object might have been to add a "save" method that executes some SQL (structured query language):

class User
{
    public $id;
    public $name;
    public $email;

    public function __construct($container, $name = null, $email = null)
    {
        $this->db = $container['db'];
        $this->name = $name;
        $this->email = $email;
    }

    public function save()
    {
        // If this user already exists in the DB, then just update it.
        if (isset($this->id)) {
            $this->db->query("UPDATE users SET name = :name, email = :email WHERE id = :user_id");
            $this->db->bind(':name', $this->name);
            $this->db->bind(':email', $this->email);
            $this->db->bind(':id', $this->id);
            $this->db->execute()
        }
        // Otherwise create a new user record.
        else {
            $db->query("INSERT INTO users VALUES (NULL, :name, :email)");
            $this->db->bind(':name', $this->name);
            $this->db->bind(':email', $this->email);
            if ($this->db->execute()) {
                $this->id = $db->lastInsertId();
            }   
        }
    }
}

Here we've changed the code to allow us to pass in a container with a database adaptor, and we either insert or update a user in the database when the save() method is called. Already you can see that this is quickly adding a lot of lines of code to our user model, and this is for a simple example which only contains a name and email. What's more, we haven't even added any code yet for fetching users from the database! We only added code to save a user.

The goal of an ORM like ADM is to free you from having to implement methods to save and grab the model data from the database. This not only allows you to get rid of a lot of code clutter from your classes, it also saves you time by freeing you from having to implement error-prone, busy-work like writing all those SQL queries. Using ADM, your model and the controller code to create a new user would look something like this:

class User extends \Cora\Model {

    public $model_attributes = [
        'id' => [],
        'name' => [],
        'email' => []
    ];

    public function __construct($name = null, $type = null)
    {
        $this->name = $name;
        $this->type = $type;
    }
}


///////////////////////////////////////////
// Controller code to create a new user
///////////////////////////////////////////
class Users extends \MyApp {
    public function register()
    {
        $repo = \Cora\RepositoryFactory::make('User');
        $user = new User('testUser', 'testUser31@gmail.com');
        $repo->save($user);
    }
}

Let's quickly discuss ActiveRecord vs. Data-Mapper implementations which will then segway into how ADM is implemented in the next section.

ActiveRecord Pattern

In terms of functionality, ActiveRecord basically works just like the example from the above section where I showed what your code might look like if you implemented handling database persistance on your own. The difference being that your model will extend from a base class that handles the database related logic for you. So you won't have to write (typically) the SQL queries to save your objects, but the logic will still be a part of your model. ActiveRecord generally looks like this:

class User extends ActiveRecordModel
{
    public $name;
    public $email;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }
}

class ActiveRecordModel
{
    // Save record to database.
    public function save()
    {
        // SAVING TO DB LOGIC
    }

    // Fetch record from database.
    public function fetch($parameters)
    {
        // FETCHING FROM DB LOGIC
    }
}

There's a few different main complaints people have about the ActiveRecord pattern, and they're all kinda interrelated. Let's start by mentioning its lack of SRP adherance. SRP stands for Single Responsibility Principle, and basically means each class should only handle a single part of a software's functionality. In doing so, you help your app stay logically sensible and readable, and make testing easier. If software doesn't adhere to the SRP, then it runs the risk of becoming a confusing mess of spaghetti code that's hard to test, hard to understand, hard to upgrade and maintain, etc.

This of course leads into and overlaps the other two main complaints about AR, those being that it can be hard to test (depends on just how much "magic" happens under the surface) and that there's generally a lack of separation between the domain (aka 'the app logic') and the storage medium (database). The testing issues usually are the result of "magic" in the form of data being passively fetched from a database, which if you're trying to test your app independent of the storage medium, can obviously be frustrating. Similarly, the lack of domain/database separation also stems from how closely tied to the database the ActiveRecord pattern is coupled by nature of how it works.

Why is important to keep the domain (app) logic decoupled from the database? Several reasons, the most important probably being:

On the flip side, ActiveRecord's strengths are that it's super easy to use and requires little to no setup (which are awesome positives!). For these reasons, it's an extremely popular ORM pattern used by many frameworks.

Data-Mapper Pattern

The Data-Mapper pattern utilizes a completely different approach from anything we've talked about so far. When using a data mapper, your models are COMPLETELY SEPARATED from the storage layer. They don't know how to save themselves, or fetch records, or anything else, because handling persistence to a storage medium is outside their responsibility! Using this pattern, it's the Data Mapper's job to know how to save and fetch objects. It 'maps' different kinds of data used in your app to some sort of storage point. This is obviously a complete 180 from the ActiveRecord pattern where objects have to know how to fetch and save from the storage layer themselves.

An ultra simplistic representation of the Data Mapper pattern might look like the below. Notice the User class doesn't extend anything, and a User object must be passed in to the Data Mapper for it to save the user.

class User
{
    public $name;
    public $email;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }
}

class DataMapper
{
    // Save record to database.
    public function save($objectToBeSaved)
    {
        // SAVING TO DB LOGIC
    }

    // Fetch record from database. Some options are passed in to determine what to fetch.
    public function fetch($filterParameters)
    {
        // FETCHING FROM DB LOGIC
    }
}

As SRP purists will delight to tell you, the data mapper pattern adheres to the Single Responsibility Principle quite well. Furthermore, because an object is simply an object and has nothing magical or special about it, this makes testing way more straightforword. Need to save an object to a different storage medium? You have the option of just passing the object to a different data mapper! Need to map parts of your object to one storage medium, and another part of the same object to a different storage location? Using Data Mapper you can write the functionality to handle this without adding all sorts of unrelated logical clutter to your models! In these and other ways, the Data Mapper pattern is much more flexible than ActiveRecord.

The downsides to this Data Mapper pattern are that it can be a little hard to understand and use, and it can require more setup time and thought because it doesn't necessarily have the 1-to-1 type of relationship with a database many AR implementations possess.

Understanding ADM's Implementation

WARNING: The following two paragraphs are chock-full of fancy terminology you may not understand. Don't freak out! Just keep reading and all will be explained by the end of this section.

Alright, with the understandings from the previous section in place, we're now ready to talk about how ADM implements object relational mapping. In short, ADM uses a generalized Repository-Gateway-Factory design pattern where the Gateway (which acts as the mapper between the domain and the persistence layer) asks the model for various pieces of information about itself and the data it has ownership over. The Gateway does this via the utility methods the Cora Model class provides. This separation of concerns with the Gateway acting as the mapper between the persistence layer and the domain, is in-line with using a Data Mapper and provides you with all the benefits of that pattern.

However, ADM, as discussed previously, is not just a pure Data-Mapper implementation, but instead a blend of both ActiveRecord and Data-Mapper. The goal of ADM is to provide the ease of use ActiveRecord is famous for, but also the separation of concerns and flexibility that Data Mapper excels at. The way ADM accomplishes this is by using the repository-gateway-factory pattern AND by having its models inherit methods from Cora's Model class. This inheritance is obviously similar to ActiveRecord, however, unlike AR, those methods do not directly interact with the persistence layer. Instead, they help other classes (particularly the Gateway) understand and use the attributes defined in the model. They also provide the model with knowledge of the repository's existence, so the model can ask the repository to fetch data dynamically (Click Here to read about how that works).

PHEWWW! Was that a mouthful or what? Hold on a sec, I think I need to cram a few more buzzwords in those two paragraphs... Seriously though, now that's I've given the highly compact description of how ADM works, let's slow down a bit and kinda walk through it bit-by-bit.

So there's really two different logic flows that are important for understanding exactly what's going on, the regular logic flow of fetching objects from a repository, and the logic flow of fetching related objects dynamically. First let's look at what happens when we want to fetch from a repository.


Fetching from a Repository

(typical Data-Mapper type implementation):

 

The Code (an example of a User repository in use within a Controller):

// Repository Setup (if not using RepositoryFactory)
$db = \Cora\Database::getDb('name_of_db_connection');
$factory = new Factory('User');
$gateway = new Gateway($db);
$repo    = new Repository($gateway, $factory);

// Grab a user from repository. This triggers the logic flow seen in the diagram below.
$user = $repo->findBy('email', 'Bob@fakeEmail.com');
 

The Logic Flow Chart - Diagram A:

 

repository_pattern

 

As can be seen, there's nothing too fancy here. This is a normal looking Repository pattern, where the Gateway is acting as our Data Mapper. If you aren't familiar with it, here's a description of what the objects responsibilities are:


Next let's look at the logic flow of fetching related objects dynamically. On the surface this might seem like a small change (judging from the diagram), but really it's much different as in this scenario a model itself is the one creating and using a Repository (via the methods built into its Cora Model class parent).


(still Data-Mapping, but incorporates ActiveRecord type use of Cora Model class)

 

The Code (accessing a related object to a User):

// Repository Setup (this time we're using RepositoryFactory)
$repo = \Cora\RepositoryFactory::make(User::class);

// Grab a user from repository. This triggers the logic flow seen in the diagram A.
$user = $repo->findBy('email', 'Bob@fakeEmail.com');

// Access a related object (Job is a class). This triggers the logic in diagram B below.
echo $user->job->title;
 

The Logic Flow Chart - Diagram B:

 

dynamic_pattern

 

The only difference between diagram A and diagram B above is how the repository gets invoked (steps 0 through 1). However, this is actually a pretty major change in the logic flow! When normally fetching from a repository, there's no interaction with a model, a controller asks a repository for one or more objects and the repository returns them. When a controller already has a Cora model and needs to grab a related object, that's when the logic built into the Cora model class kicks in to facilitate. The model looks at its own definition and any data passed in when it was created, and from those determines how to create a Repository to grab the needed related object. This is how ADM implements ActiveRecord like simplicity in fetching objects, while still maintaining separation of concerns. The model doesn't directly interact with the persistence layer (knowing that's not its responsibility), but rather the Cora Model class gives it the necessary knowledge on how to use the Repository system. And then, just to make sure testing doesn't get difficult, options are included to disable Dynamic Fetching, or to pass in a different database adaptor to override any defaults (I.E. force dynamic fetching to use a test database). See Here for more information on testing.


Ok, that's it! Hopefully you have a high level understanding of how ADM blends together the best of Data-Mapper and ActiveRecord into a new best-of-both worlds system.