Skip to content

Agile Data for Doctrine users

Romans Malinovskis edited this page Aug 16, 2016 · 7 revisions

This document explains Agile Data concepts using similarities from Doctrine-ORM.

Agile Data and Doctrine are both designed to abstract database access, deal with impedance mismatch, persistence separation and extension implementation (e.g. Sortable).

Overview

Model in Agile Data is a base class that Entities must extend. Objects of a Model class can be used similar to associative array:

$m['name'] = "John";

// or 

$m->set('name', 'John');

Each model defines method init() that is called when Model is associated with a specific Persistence (SQL, Array or REST). The init() method will define the following:

  • Field - defines Fields and their parameters (such as mapping, type, default values)
  • Join - defines how to join tables for CRUD operations
  • Conditions - similar to Filter Constraint
  • Hooks - similar to Behaviours

using Model methods (addField, join, addCondition, addHook). Those methods may use different 'Classes' for creating dependent objects based on persistence:

  • Persistence_Array - addField() creates new object of 'Field' class.
  • Persistence_SQL - addFiled() creates new object of 'Field_SQL' class, which knows a bit more about SQL.
  • Join_Array - implements join between static in-memory arrays.
  • Join_SQL - implement join inside queries.

Conditions and Hooks don't create a new object but are stored as an array internally inside Model. I must note that once model is associated with persistence, it cannot be detached.

Models are mutable and while some operations can be reversed, others cannot (by design).

Associating Model with SQL-capable persistence can unlock some extensions such as 'Expressions' or 'Transactions'. There is also concept for 'Actions', and supported actions may vary depending selected persistence engine.

Conditions and Data Sets

The Model supports permanent conditions for your entities. This operation limits which records Model can load from Persistence. The set of all those "loadable records" is called DataSet and is a unique feature of Agile Data.

Once the condition is added to the model object, you won't be able to save, update, delete or execute other operations on records outside the DataSet (for safety reasons).

Relations

Although they may appear similar to Associations, Relations in Agile Data are quite different.

  • Relation is always defined between entities (never between tables)
  • Relations can exist between different presistencies (SQL to Mongo)
  • Relations are designed around "business logic" rather than "foreign keys in the database"
  • There may be many relations between two models at the same time

Relations can be defined inside the init() method or even in-line. Each relation has a unique name, which must be specified while traversing.

$user->hasMany('Order');
$order = $user->ref('Order');

Cardinalities and Traversal

Agile Data never operates with the arrays of entities or collections. It relies on "DataSet" and in-line "Conditions" to describe a set of "Related" entities through a DataSet. In my last example both $user and $order are models. If user has an Active record, then DataSet of $order will be conditioned to the orders of that specific user.

If $user does not have an active record selected, then DataSet of $order will match all orders that correspond to the DataSet of a $user.

$vip_users = new Model_User($db);
$vip_users->addCondition('is_vip', true);
$vip_orders = $vip_users->ref('Order');

Defining one relation can re-use other relations:

$user->addRelation('ExpiredOrder', function($m){
    return $m->ref('Order')->addCondition('is_expired', true);
});

To summarise:

  • Relations can add Conditions for destination Model
  • Conditions can use data of an active record
  • Traversing Relation always generates a new Model

By default, there are 'hasOne' and 'hasMany' relations, but you can introduce new relation classes.

Many to Many

Agile data does not really need many diverse types of relationships, and can survive with just the two basic ones. To illustrate how many-to-many would work, suppose you have 3 models:

  • User
  • System
  • UserSystemAccess

User relates to UserSystemAccess (one to many) but does not know anything about System and vice versa. In Agile Data you can write:

$user->load(10);
$user_sytems = $user->ref('UserSystemAccess')->addCondition('role', 'admin')->ref('system_id');

This is called deep traversal and will return object of a model 'System' that has inline condition:

  • where user_system_access_id in (select id from user_system_access where user_id = 10 and role='admin')

Expressions

Agile Data is perfectly capable when operating around expressions. The above example was using expression to define values for "user_system_access_id" without actually loading list of IDs. Similarly, expressions can be used in many other places, such as when inserting records:

$m = new Model_User($db);
$m['country_id'] = $m->ref('country_id')
    ->addCondition('name','UK')
    ->action('field', ['id']); 
$m->save();

The save() will perform single query: insert into user (.., country_id) values (.., select id from country where name="UK"))

While similar can be achieved with Doctrine_Expression, the action() will imply some important conditions or joins into sub-query to maintain data integrity.

Actions

Models in Agile Data offer much more than just CRUD operations. Calling $model->action will return an Expression or Query that can be used for many things like aggregating:

$total_vip_orders = $vip_users->ref('Order')->action('fx', ['sum', 'total'])->getOne();

Actions can also be used to define "virtual" aggregate fields. If you read documentation on aggregate fields in Doctrine it first introduces a way how to calculate account balance using DQL. Just like my example above, DQL can be used to calculate total of all orders for a specific case.

Agile Data allows you to convert your "action" inside an aggregate field:

$user->addExpression('total_orders')->set($user->refLink('Order')->action('fx', ['sum', 'total']));

$user->load(123);
echo $user['total_orders'];

Workflow

Defining Database Structure

You normally don't reverse-engineer the schema in Agile Data but are asked right from the start to think in terms of Business Entities. If the record in table user cannot exist without user_details record, then your 'Model_User' will be defining join between both tables and the rest of your business logic will honour the association. Once defined like that it will be impossible to create stray records in user records.

Multiple business models can use the same table, without a common ancestor. When extending model you can even specify a different table.

CRUD operations

Once your model object is associated with Persistence, you can re-use it to load various records:

$m = new Model_User($db);
$m->load(3);
$m->set('name', 'John');
$m->save();

$m->load(5)->delete();

$m->addCondition('is_vip', true);
$m->loadAny();
var_dump($m->get());

Inline definitions

Agile Data is unique in allowing you to create on-the-fly definitions for your Models. This can be used by developer for data integrity:

$m = new Model_User($db);
$m->addCondition('is_confirmed', true);
$m->loadBy('email', $email);
$m->verifyPassword($password);

and is extensively used by Relations.