Model Class

The purpose of Cora's Model class is to provide methods that allow Gateways to examine the properties and relations of models, and to give models the knowledge of the Repository system's existence and the Repository's responsibility for grabbing and persisting models. This enables the ActiveRecord like features of ADM such as dynamic loading.

Creating a Cora Model

To create a Cora Model, you just need to have your model extend \Cora\Model like such:

class SomeModel extends \Cora\Model {}

However, to actually make your model do anything useful, you need to define your model using some data members so that ADM knows how to use it. The core of this definition, is the "model_attributes" array.

Attributes

tl;dr - A Cora model's attributes define the data that composes the model.

Normally when talking about data members for a class in PHP, the term "properties" is used. However, since it's possible to define properties on a Cora model just like you would any other class, "attributes" is the term I've coined to describe those model properties that relate directly to ADM so that the two don't get confused.

Let's jump right in to how it works, as it's easier to describe using examples.

How it Works

Let's say you have a model definition that looks like the following:

class User extends \Cora\Model {

    // Normal properties
    // These WON'T be saved to the database.
    protected $lastAction;

    // ADM properties
    // aka the model's "Attributes"
    // These WILL be saved in the database.
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'email' => [
            'type' => 'varchar'
        ],
        'type'  => [
            'type' => 'varchar'
        ]
    ];

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

    public function changeName($newName) {
        $this->name = $newName;
        $this->lastAction = 'Changed Name';
    }

    public function getName() {
        return $this->name;
    }

    public function getLastAction() {
        return $this->lastAction;
    }
}

In the example, we've defined a normal property called "lastAction" which we're using to keep track of the last action that was performed by/on that user. However, since we defined it as a normal property and not a model attribute, that means the lastAction WILL NOT get saved to the database if we save the model. That's super important to understand! You can use normal class properties, but only those attributes defined in the $model_attributes property will be persisted (saved). This makes the "$model_attributes" property the backbone that supports all of ADM's functionality.

In terms of implementation, as can be seen in the example above, $model_attributes is just an associative array where the key is the name of the attribute, and the value is another associative array for defining that attribute's details. The most basic detail which I always recommend including is that attribute's "type". By defining this, you can use ADM's automated database creation tool.

Definition

Below are the basic properties you can use to describe an attribute definition.

Attribute Name Valid Values Description
type
tinyint, smallint, mediumint, int, bigint
varchar, char,
float,
date, datetime,
boolean,
binary,
tinytext, text, mediumtext, longtext
tinyblob, mediumblob, blob, longblob
For setting the "type" field in a DB.
size {integer} The size of the field in the DB. If size is not set and the type is 'varchar', size will default to 255 when using MySQL adaptor.
precision {integer} If the type is a float, this sets the places after the decimal point.
primaryKey true This should only be set for ONE attribute per model.
autoIncrement true Is the field auto-incrementing in value?
defaultValue 'Some String' Sets a default value for this attribute. If this value needs to be an object, or dynamically generated, then use the beforeCreate() lifecycle callback to assign a default value. See the lifecycle callbacks section for more info.
enum 'value1', 'value2', 'value3' The valid values if the field type is set to 'enum'. I don't actually recommend you use ENUM if you're starting a new project (it may be hard to support across various database types). This feature is mostly present for integration with existing projects.
unique true Tells the database that the values of this attribute should be unique.
index true Make an index for this attribute.

For a slightly more detailed example, let's modify our User definition from above for the user 'type' attribute. Let's say we know we don't need 255 characters (the default) for the type, and also we want the default user type to be just 'User'. Furthermore, let's say we know we'll be searching a lot by email, so we want to index the email attribute:

public $model_attributes = [
    'id' => [
        'type'          => 'int',
        'primaryKey'    => true
    ],
    'name' => [
        'type' => 'varchar'
    ],
    'email' => [
        'type' => 'varchar',
        'index' => true
    ],
    'type'  => [
        'type' => 'varchar',
        'size' => 55,
        'defaultValue' => 'User'
    ]
];

Now if you generate a database using ADM's database generation tool, it will reflect these changes.

Object Relations

Alright, hopefully the general gist of how to define a model's attributes is becoming clearer. An important thing we haven't addressed yet is how to setup relationships with other objects. This is where things get exciting and Cora's ADM really shines. Going back to our User object example, let's say a user has a Location associated with them as well as a number of articles they've written (maybe we're running a blogging site or something). We would setup this relationship like so:

public $model_attributes = [
    'id' => [
        'type'          => 'int',
        'primaryKey'    => true
    ],
    'name' => [
        'type' => 'varchar'
    ],
    'email' => [
        'type' => 'varchar',
        'index' => true
    ],
    'type'  => [
        'type' => 'varchar',
        'size' => 55,
        'defaultValue' => 'User'
    ],
    'location' => [
        'model' => 'location'
    ],
    'articles' => [
        'models' => 'article'
    ]
];

That's it! Notice the plural 's' on 'models' for referring to articles the user has written, and the singular 'model' for representing the user's location.

Of course, now you may be wondering how to actually USE this, but that's equally as intuitive and easy. Going back to the non-relational attributes like 'Type', if you had an existing user and you wanted to change their user type from 'User' to 'Moderator', you would do that like this:

// Grab user. 'repo' is a User repository.
$user = $this->repo->find($id);

// Change their type
$user->type = 'Moderator';

// Save the change
$repo->save($user);

Working with related objects is the same concept! Let's associate a new Location and set of Articles with a user:

// Grab user:
$user = $this->repo->find($id);

// Associate a location to this user:
$user->location = new Location('Fuel Medical LLC', 'Camas', 'WA');

// Associate a set of Articles to this user:
$user->articles = new \Cora\Collection([
  new Article('How to Write Good Code'),
  new Article('How to Unit Test')
]);

// Save the changes
$repo->save($user);

For basic usage with a single database, that's pretty much all you need to know. However, if you're an advanced user and will have your data spread across multiple databases and/or you want more control over the setup of the database (like maybe you're integrating Cora into an existing database), then read on.

Using Multiple Databases

The database config file for Cora holds an array that let's you define multiple database connections. If you've gone through the setup process, you've undoubtably seen it, but here's a reminder of how this looks:

$dbConfig['defaultConnection'] = 'mainDb';
$dbConfig['connections'] = [
    'mainDb' => [
        'adaptor'   => 'MySQL',
        'host'      => 'localhost:3306',
        'dbName'    => 'cora',
        'dbUser'    => 'root',
        'dbPass'    => 'root'
    ],
    'secondaryDb' => [
        'adaptor'   => 'MySQL',
        'host'      => 'localhost:3306',
        'dbName'    => 'cora2',
        'dbUser'    => 'root',
        'dbPass'    => 'root'
    ],
    'mongoDb' => [
        'adaptor'   => 'MongoDb',
        'host'      => 'localhost:3306',
        'dbName'    => 'cora3',
        'dbUser'    => 'root',
        'dbPass'    => 'root'
    ]
];

In this config file you define the connection details for each database, tell it what Cora database adaptor to use with that connection, and give each connection a name.

In order to make use of multiple connections, you have to specify that some of your models use a connection which is different from the default. Like so:

class Guide extends MyModel {

    public $model_connection = 'secondaryDb';
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'title' => [
            'type' => 'varchar'
        ],
        'authors' => [
            'models' => 'user'
        ]
    ];

    public function __construct($title = null)
    {
        $this->title = $title;
    }

}

Now when you generate your database, the Guides table/collection will get created using the 'secondaryDb' connection instead of the main one. Similarly, grabbing Guide object from the DB using a repository will use this secondaryDb connection too:

$repo = \Cora\RepositoryFactory::make('Guide');
$guide = $repo->find($id);

The spot where you could run into trouble is when you want a two-way reference between objects in different databases. For example, looking at our Guide definition above you can see we have a reference to one or more User models representing the authors of said guide. But what if our Users model also keeps a reference to Guides a user has been an author of? Let's add a reference to Guides in our User model:

Note: Because our User model was defined as using the default connection (we didn't tell it to use anything different) ADM will know to fetch the authors from the main database.

public $model_attributes = [
    'id' => [
        'type'          => 'int',
        'primaryKey'    => true
    ],
    'name' => [
        'type' => 'varchar'
    ],
    'email' => [
        'type' => 'varchar',
        'index' => true
    ],
    'type'  => [
        'type' => 'varchar',
        'size' => 55,
        'defaultValue' => 'User'
    ],
    'location' => [
        'model' => 'location'
    ],
    'articles' => [
        'models' => 'article'
    ],
    'guides' => [
        'models' => 'guide'
    ]
];

So now we have a reference to Users from a Guide, and a reference to Guides from a User. The way these are related is by using a relation table. If you aren't familiar with relation tables, see the image below for the general idea. Basically it's a really simple table that holds relationships (shocking right!?). In this screenshot, guides 9 and 10 are associated with a user who's ID is '1', and guide 11 is associated with whoever user '2' is:

dynamic_pattern

The problem lies in the question of "Which database is going to hold the relation table?" Having to maintain redundant tables on each database and then remembering to alter both anytime a relationship changes is just plain dumb, and assuming that the relationship will be kept in the default database didn't feel like an assumption I could make for sure either. Thus, in order to solve this problem I've introduced the concept of 'passive' sides in a multi-database relationship.

To fix our User & Guide model definitions as we've defined them above, we need to mark one side of that relationship as the 'passive' side like so:

class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'email' => [
            'type' => 'varchar',
            'index' => true
        ],
        'type'  => [
            'type' => 'varchar',
            'size' => 55,
            'defaultValue' => 'User'
        ],
        'location' => [
            'model' => 'location'
        ],
        'articles' => [
            'models' => 'article'
        ],
        'guides' => [
            'models' => 'guide',
            'passive' => true
        ]
    ];
}

By marking the User side of the relationship as the passive side, ADM will now know to make the relationship table on the database represented by Guide's connection.

Advanced Configuration

Alright, using ADM's automated database creation feature and simply setting relationships as singular "model" or plural "models" and letting ADM handle the rest is nice and all for new projects. But let's talk about configuration options for incorporating ADM into a pre-existing database where you have to work within the constraints of the existing setup.

First off, let's talk table names.

Change the table to which a model is mapped

By default, a table/collection name will be the plural version of the model. For instance: our User model will be stored in a table called "users". Our Guide model will be stored in a table called "guides" and so on and so forth. If this behavior won't work with your existing database for a particular model, you have the option to specify a different name.

Let's say you are building an internal app to be used by your company and you want the User model to map to a table named "employees". You would specify that like so:

class User extends \Cora\Model {

    public $model_table = 'employees';
    public $model_attributes = [
        ... stuff here ...
    ];
}

Change the name of a relation table

Whenever you specify a simple plural relationship from one model to another like so:

class User extends \Cora\Model {

    public $model_attributes = [
        'articles' => [
            'models' => 'article'
        ]
    ];
}

A relation table is used that is named by making the models involved plural and appending them together with an underscore in reverse alphabetical order. In the example above of a User associated with an Article the resulting relation table name would be "users_articles".

If you're fine with the use of a relation table, but you need to change the name of it. You can do so by utilizing the 'relTable' setting like this:

class User extends \Cora\Model {

    public $model_attributes = [
        'articles' => [
            'models' => 'article',
            'relTable' => 'some_custom_relation_table_name'
        ]
    ];
}

The one caveat here is that at the time of this writing, I do not support custom column names within the relation table. The relation table column names must be the name of the model who's ID they hold. For the above example with User and Article, the relation table will need a 'user' column and an 'article' column. If you have existing relation tables that don't follow this format, you'll have to change them or else not use ADM.

One-to-Many via an "owner" column

The default setup for plural relationships in ADM is the use of relation tables just because they are the most flexible. However, if you want to force the use of an "owner" type column in a one-to-many relationship either because it's your personal preference or it's necessary to integrate with an existing database, ADM makes this possible.

What you'll need to do is just throw a 'via' setting in with a plural model definition. For instance, let's say we don't care about supporting multiple authors for an article on our site, but rather just want an article to have a single owner and we want to store a reference to this owner in the articles table via an "owner" column. We do that like this:

class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'email' => [
            'type' => 'varchar',
            'index' => true
        ],
        'type'  => [
            'type' => 'varchar',
            'size' => 55,
            'defaultValue' => 'User'
        ],
        'location' => [
            'model' => 'location'
        ],
        'articles' => [
            'models' => 'article',
            'via'    => 'owner'
        ]
    ];
}

One-to-One using a relation table

By default, one-to-one relationships are stored as a column on the model's table. For instance, the following definition for a related Job object would be stored as seen in the picture below:

class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'email' => [
            'type' => 'varchar',
            'index' => true
        ],
        'type'  => [
            'type' => 'varchar',
            'size' => 55,
            'defaultValue' => 'User'
        ],
        'job' => [
            'model' => 'job'
        ]
    ];
}

"users" table:
dynamic_pattern

However, if for some reason you wanted to force a single reference to use a relation table (maybe you felt it could change to a plural relationship in the future or something), you could do so by specifying "usesRefTable" as true:

class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'email' => [
            'type' => 'varchar',
            'index' => true
        ],
        'type'  => [
            'type' => 'varchar',
            'size' => 55,
            'defaultValue' => 'User'
        ],
        'job' => [
            'model' => 'job',
            'usesRefTable' => true
        ]
    ];
}

Optionally you can of course combine this with the previously talked about "relTable" option to specify the name of the relation table if the default naming doesn't work for you.

Namespaces

Just a quick explanation of namespaces in the model definition...

When defining related models in your $model_attributes array, namespaces are always absolute, never relative. See the namespaced Notes model below and the relationship definition added to User for an example:

<?php
namespace Task;

class Note extends \Note {

    public $model_table = 'notes_tasks';
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'title' => [
            'type' => 'varchar'
        ],
        'note' => [
            'type' => 'text'
        ],
        'owner' => [
            'model' => 'user'
        ]
    ];
}
class User extends \Cora\Model {
    public $model_attributes = [
        'id' => [
            'type'          => 'int',
            'primaryKey'    => true
        ],
        'name' => [
            'type' => 'varchar'
        ],
        'email' => [
            'type' => 'varchar',
            'index' => true
        ],
        'type'  => [
            'type' => 'varchar',
            'size' => 55,
            'defaultValue' => 'User'
        ],
        'notes' => [
            'models' => 'task\\note',
            'via'    => 'owner'
        ]
    ];
}