/moa

MOA implements dynamically generated Active Record database abstraction.

Primary LanguagePHPOtherNOASSERTION

MOA

Build Status Coverage Status Latest Stable Version License

This project is no longer maintained.

MOA (Mother of All) is a database abstraction using Active Record pattern:

Active record is an approach to accessing data in a database. A database table or view is wrapped into a class. Thus, an object instance is tied to a single row in the table. After creation of an object, a new row is added to the table upon save. Any object loaded gets its information from the database. When an object is updated the corresponding row in the table is also updated. The wrapper class implements accessor methods or properties for each column in the table or view.

http://en.wikipedia.org/wiki/Active_record_pattern

MOA is designed to handle CRUD operations.

MOA is not ORM. MOA does not work with object relations and dependencies. However, these libraries do:

MOA does not implement elaborate finders, filters or methods for querying data. However, these libraries do:

Hierarchy & Responsibilities

Builder

MOA is using dynamic code generation to represent your database. builder script generates a file for each table using attributes fetched from the database (e.g. column name, type, default value, etc.). These classes are generated dynamically to reduce the amount of hand-coded duplication of the data representation.

This is an example of generated class.

With other Active Record implementations you do not need a generator because these properties are either hand-typed or fetched during the execution of the code. The former is tedious and error-prone, while the latter is a lazy-workaround that has a considerable performance hit.

Mother

All models extend Gajus\MOA\Mother. Mother attempts to reduce the number of executions that would otherwise cause an error only at the time of interacting with the database. This is achieved by using the pre-fetched table attributes to work out when:

  • Accessing a non-existing property.
  • Setting property that does not pass derived or custom validation logic.
  • Saving object without all the required properties.

Furthermore, Mother is keeping track of all the changes made to the object instance. UPDATE query will include only the properties that have changed since the last synchronization. If object is saved without changes, then UPDATE query is not executed.

If you know a negative downside of the behavior described above, please contribute a warning.

Delete operation will remove the object reference from the database and unset the primary key property value.

Hierarchy

Using MOA you can choose your own namespace) and have your own base class.

This is an example of an application hierarchy incorporating all of the MOA components:

Gajus\MOA\Mother
    you base model [optional]
        MOA generated models
            your hand-typed models [optional]
                your hand-typed domain logic [optional]

API

This section of the documentation is using code examples from a fictional application to introduce you to the API. The My\App\Model\Person model in the example extends from:

/**
 * This class is generated using https://github.com/gajus/moa.
 * Do not edit this file; it will be overwritten.
 */
abstract class Person extends \Gajus\MOA\Mother {
    const TABLE_NAME = 'person';
    const PRIMARY_KEY_NAME = 'id';

    static protected
        $columns = [
            'id' => [
                'column_type' => 'int(10) unsigned',
                'column_key' => 'PRI',
                'column_default' => NULL,
                'data_type' => 'int',
                'is_nullable' => false,
                'extra' => 'auto_increment',
                'character_maximum_length' => NULL,
            ],
            'name' => [
                'column_type' => 'varchar(100)',
                'column_key' => '',
                'column_default' => '',
                'data_type' => 'varchar',
                'is_nullable' => false,
                'extra' => '',
                'character_maximum_length' => 100,
            ],
            'language' => [
                'column_type' => 'varchar(100)',
                'column_key' => '',
                'column_default' => 'English',
                'data_type' => 'varchar',
                'is_nullable' => false,
                'extra' => '',
                'character_maximum_length' => 100,
            ]
        ];
}

Create and Update

Object is inserted and updated using save method. Object is inserted to the database if instance primary key property has no value. Otherwise, object is updated using the primary key property value.

/**
 * @param PDO $db
 * @param int $id
 */
$person = new \My\App\Model\Person($db);

// Set property
$person['name'] = 'Foo';

// Insert object to the database
$person->save();
# $person['id'] 1

When object is inserted to the database, new object state is fetched from the database:

// Note that "language" property was not set,
// though it had default value in the table schema.
# $person['language'] English

// Update property
$person['name'] = 'Bar';

// Save object state to the database
$person->save();
# $person['id'] 1

Delete Object

Deleting object will remove the associated entry from the database and unset the primary key property value.

$person->delete();
# $person['id'] null

However, other property values are not discarded. If the same object instance is saved again, it will be inserted to the database with new primary key value:

# $person['name'] Bar
$person->save();
# $person['id'] 2

Inflate Object

Object is inflated using the primary key:

$person = new \My\App\Model\Person($db, 2);

In the above example, object data is retrieved from the database where primary key value is "2".

Getters and Setters

MOA implements ArrayAccess interface. You can manipulate object properties using the array syntax, e.g.

<?php
$person = new \My\App\Model\Person($db);
$person['name'] = 'Baz';
$person->save();

or if you need to set multiple properties at once:

/**
 * Shorthand method to pass each array key, value pair to the setter.
 *
 * @param array $data
 * @return Gajus\MOA\Mother
 */
$person->populate(['name' => 'Qux', 'language' => 'Lithuanian']);

Extending

Mother

To inject logic between Mother and the generated models:

  1. Extend Gajus\MOA\Mother class.
  2. Build models using --extends property.

Individual models

Models generated using MOA are abstract. You need to extend all models before you can use them:

<?php
namespace My\App\Model;

class Person extends \Dynamically\Generated\Person {
    static public function get[Where][..] (\PDO $db) {
        $person_id = $db
            ->query("SELECT `" . static::$properties['primary_key_name'] . "` FROM `" . static::$properties['table_name'] . "` ORDER BY `[..]` DESC LIMIT 1")
            ->fetch(\PDO::FETCH_COLUMN);

        if (!$person_id) {
            throw new \Gajus\MOA\Exception\RecordNotFoundException('[..]');
        }
        
        return new static::__construct($db, $person_id);
    }

    static public function getMany[Where][..] (\PDO $db) {
        $sth = $db->prepare("SELECT * FROM `" . static::$properties['table_name'] . "` WHERE `[..]` = ?");
        $sth->execute(/* [..] */);

        return $sth->fetchAll(\PDO::FETCH_ASSOC);
    }
}

MOA convention is to prefix "getMany[Where]" methods that return array and "get[Where]" that return an instance of Mother. This is not enforced. It is an observation of what works the best in practise.

Triggers

These methods can interrupt the respective transactions:

/**
 * Triggered after INSERT query but before the transaction is committed.
 * 
 * @return void
 */
protected function afterInsert () {}

/**
 * Triggered after UPDATE query but before the transaction is committed.
 * 
 * @return void
 */
protected function afterUpdate () {}

/**
 * Triggered after DELETE query but before the transaction is committed.
 * 
 * @return void
 */
protected function afterDelete () {}

Validation

MOA ensures that user input is compatible with the schema, e.g. if input will be truncated because it is too long.

MOA provides two types of validation that you can implement before the schema validation.

/**
 * Triggered when an attempt is made to change object property.
 * Returning an error message will discard the transaction and throw Gajus\MOA\Exception\ValidationException exception.
 * 
 * @param string $name
 * @param mixed $value
 * @return null|string
 */
protected function validateSet ($name, $value) {}

/**
 * Triggered when an attempt is made to save object state.
 * Returning an error message will discard the transaction and throw Gajus\MOA\Exception\ValidationException exception.
 * 
 * @return null|mixed
 */
protected function validateSave () {}

Naming Convention

  • MOA model names are using CamelCase convention (e.g. UserAgent).
  • Table names must be singular (e.g. user_agent not user_agents) using underscore convention.

Builder Script

Models are built using ./bin/build.php script, e.g. unit testing dependencies in this repository are built using:

php ./bin/build.php\
    --namespace "Sandbox\Model\MOA"\
    --database "moa"\
    --path "./tests/Sandbox/Model/MOA"

Parameters

Name Description
path [required] Path to the directory where the models will be created.
database [required] MySQL database name.
host MySQL database host.
user MySQL database user.
password MySQL database password.
namespace [required] PHP class namespace;
extends PHP class to extend. Defaults to "\Gajus\MOA\Mother".

All .php files will be deleted from the destination path. The destination path must have an empty .moa file. This requirement is a measure to prevent accidental data loss.

Installation

MOA uses Composer to install and update:

curl -s http://getcomposer.org/installer | php
php composer.phar require gajus/moa