Read Models.

Add active-record style, read-only models to a Doctrine project for easier querying and separation of presentation from data.

Article image for: Read Models

Introduction.

Introduction
Querying
Type Casting
Relationships
Identity Map
Exporting
Profiling / Testing

Github Print

Read-Models

GitHub Actions Build Status

Read-Models are a companion resource to a Doctrine ORM entity based project. They provide an active-record style data access layer, designed for presentational purposes only. This allows your domain objects to remain completely focused on managing your data and not getting sidelined with presentational concerns.

To further highlight this tight integration, read-models uses DBAL and the DBAL type system under-the-hood. Any registered types will be used during model hydration and even embeddables can be reused.

Note that unlike standard active-record packages, there is no write support at all nor will this be added. This package is purely focused on reading and querying data with objects / query builders for use in the presentation layer.

A lot of the internal arrangement is heavily inspired by Laravels Eloquent and other active-record projects including GranadaORM (IdiORM), PHP ActiveRecord and others.

Current Features

  • active-record query model
  • read-only - no ability to change your db through the built-in methods
  • read-only models - no mutation methods, models are immutable once loaded
  • support for attribute casting
  • support for embedded objects via attribute casting
  • support for exporting as JSON / Array data (configurable)
  • relationships (1:1, 1:m, m:m, 1:m reversed)
  • identity map
  • pluggable attribute casting / cast to value-objects

Requirements

  • PHP 8.0+
  • mb_string
  • doctrine/dbal
  • somnambulist/collection

Installation

Install using composer, or checkout / pull the files from github.com.

  • composer require somnambulist/read-models
  • instantiate Manager with your connection mappings and any attribute casters
  • add models extending the Model class, being sure to set the table name property
  • load some data: <model>::find()

For example:

use Doctrine\DBAL\DriverManager;
use Somnambulist\Components\ReadModels\Manager;
use Somnambulist\Components\ReadModels\TypeCasters\DoctrineTypeCaster;

new Manager(
[
    User::class => $conn = DriverManager::getConnection(['url' => 'sqlite://db.sqlite']),
    'default'   => $conn,
],
[
    new DoctrineTypeCaster($conn),
]
);

Usage

Extend Somnambulist\Components\ReadModels\Model and add casts, define relationships, exports etc.

class User extends Model
{

    protected string $table = 'users';
}

You can add a default table alias by setting the property: $tableAlias. Other defaults can be overridden by defining the property:

class User extends Model
{

    protected string $table = 'tbl_users';
    protected ?string $tableAlias = 'u';
    protected string $primaryKey = 'uuid';

}

Note: properties are defined with types and must follow those defined in the base class.

To load a record:

$model = User::find(1);

$results = User::query()->whereColumn('name', 'like', '%bob%')->orderBy('created_at', 'desc')->limit(5)->fetch();

Access properties directly or via method calls:

$model = User::find(1);

$model->id;
$model->id();
$model->created_at;
$model->createdAt();

You cannot set, unset, or change the returned models.

You can define attribute mutators in the same way as Laravels Eloquent:

class User extends Model
{

    protected function getUsernameAttribute($username)
    {
        return Str::capitalize($username);
    }
}

// user:{username: bob was here} -> returns Bob Was Here

User::find(1)->username();

Note: these methods should be protected as they expect the current value to be passed from the loaded model attributes.

Or create virtual properties, that exist at run time:

class User extends Model
{

    protected function getAnniversayDayAttribute()
    {
        return $this->created_at->format('l');
    }
}

// user:{created_at: '2019-07-15 14:23:21'} -> "Monday"

User::find(1)->anniversay_day;
User::find(1)->anniversayDay();

Or for micro-optimizations, add the method directly:

class User extends Model
{

    public function anniversayDay()
    {
        return $this->created_at->format('l');
    }
}

// user:{created_at: '2019-07-15 14:23:21'} -> "Monday"

User::find(1)->anniversayDay();

Note: to access properties via the magic __get/call the property name must be a valid PHP property/method name. Keys that start with numbers (for example), will not work. Any virtual methods / properties should be documented using @property-read tags on the class level docblock comment. Additionally: virtual methods can be tagged using @method.

Note: to get a raw attribute value, use ->getRawAttribute(). This will return null if the attribute is not found, but could also return null for the specified key.

When returning sets of Model objects, the returned set can be customised per model to allow for specific filters on the collection or other behaviour. Override the collectionClass property with the class name to use. This class must implement the Collection contract from the somnambulist/collection project and must have extract() and add() methods.