Simple Solutions 1 – Active Record versus Data Mapper – Matthias Noback

Having discussed different aspects of simplicity in programming solutions, let’s start with the first topic that should be scrutinized regarding their simplicity: persisting model objects. As you may know, we have competing solutions which fall into two categories: they will follow either the Active Record (AR) or the Data Mapper pattern (DM) (as described in Martin Fowler’s “Patterns of Enterprise Application Architecture”, abbrev. PoEAA).

Active record

How do we recognize the AR pattern? It’s when you instantiate a model object and then call save() on it:

$user = new User('Matthias');

$user->save();

In terms of simplicity as seen from the client’s perspective, this is amazing. We can’t imagine anything that would be easier to use. But let’s take a look behind the scenes. If we’d create our own AR implementation, then the save() function looks something like this:

final class User
{
    public function __construct(
        private string $name
    ) {
    }

    public function save(): void
    {
        // get the DB connection

        $connection->execute(
            'INSERT INTO users SET name = ?',
            [
                $this->name
            ]
        );
    }
}

In order for save() to be able to do its work, we need to somehow inject the database connection object to the save(), so it can run the necessary INSERT SQL statement. Two options:

One, we let save() fetch the connection:

use ServiceLocatorDatabase;

final class User
{
    // ...

    public function save(): void
    {
        $connection = Database::getConnection();

        $connection->execute(
            'INSERT INTO users SET name = ?',
            [
                $this->name
            ]
        );
    }
}

The practice of fetching dependencies is called service location, and it’s often frowned upon, but for now this does the trick. However, the simplicity score goes down, since we have to import the service locator, and call a method on it (-2 points?).

The second option is to pass the connection somehow to the User object. The wrong approach is this:

use ServiceLocatorDatabase;

final class User
{
    // ...

    public Connection $connection;

    public function save(): void
    {
        $this->connection->execute(
            // ...
        );
    }
}

That’s because the burden of providing the Connection is now on the call site where the User is instantiated:

$user = new User();
$user->connection = /* ... get the connection */;
// ...

This would definitely cost points in the “ease-of-use” category. A better idea is to provide the connection in the framework’s bootstrap code somehow:

final class User
{
    // ...

    public static Connection $connection;

    public function save(): void
    {
        self::$connection->execute(
            // ...
        );
    }
}

// Somewhere in the framework boot phase:
User::$connection = /* get the connection from the container */;

Because we don’t want to do this setup step for every model class, and because we are likely doing similar things in the save() function of every model, and because we want each of our model classes to have a save() function anyway, every AR implementation will end up with a more generalized, reusable approach. The way to do that is to remove the specifics (e.g. the table and column names) and define a parent class that can do everything. This parent class defines a few abstract methods so the model is forced to fill in the details:

abstract class Model
{
    public static Connection $connection;

    abstract protected function tableName(): string;

    /**
     * @return array<string, string>
     */
    abstract protected function dataToSave(): array;

    public function save(): void
    {
        $dataToSave = $this->dataToSave();

        $columnsAndValues = /* turn into column = ? */;
        $values = /* values for parameter binding */;

        $this->connection->execute(
            'INSERT INTO ' . $this->tableName()
                . ' SET ' . $columnsAndValues,
            $values
        );
    }
}

// Pass the connection to all models at once:
Model::$connection = /* get the connection from the container */;

We should award ourselves several simplicity points in the area of reusability! The AR model class i

Truncated by Planet PHP, read more at the original (another 7660 bytes)

Aller à la source
Author: