Divergence uses a typical ActiveRecord pattern for it's models.
Type | Field | Description |
---|---|---|
int |
ID |
The primary key. |
enum |
Class |
Fully qualified PHP namespaced class. |
timestamp |
Created |
Time when the object is created in the database. |
int |
Creator |
Reserved for use with authentication system. |
The trait Divergence\Models\Getters
is automatically also pulled in by Divergence\Models\Model
so you don't have to do it yourself.
Trait | Description |
---|---|
Divergence\Models\Getters |
Suite of methods to pull records from the database. |
Divergence\Models\Relations |
Lets you build relationships between models. |
Divergence\Models\Versioning |
Automatically tracks history of all models. |
Classes that use ActiveRecord may optionally use traits to enable relationship features and versioning features respectively.
When using array mapping ActiveRecord will merge public static $fields
and public static $relationships
at run-time giving priority to the child. You can override fields in the child class that have already been set by a parent class.
A child class may choose to unset a relationship or field simply by setting the config to null. A child class may also use a different type for the same database field name.
Overrides must use the key for the field configuration.
You must define these seven configurables. (Don't worry most you can copy and paste.)
public static $rootClass = __CLASS__;
public static $defaultClass = __CLASS__;
public static $subClasses = [__CLASS__];
In the event that you have subclasses you can define them here. By default just use the above configuration. You'll want to override $rootClass
and $defaultClass
if necessary for yourself.
public static $tableName = 'table';
public static $singularNoun = 'table';
public static $pluralNoun = 'tables';
Table name is for the database table.
Singular noun and plural noun are mostly used by RecordsRequestHandler
to load the right template so think of those as template filenames.
As of 2.0 Divergence now supports field mapping using PHP Attributes.
For example:
#[Column(type: "integer", primary:true, autoincrement:true, unsigned:true)]
protected $ID;
Because the field is protected we still trigger __get
from outside of the object. Unfortunately this means we must manually run getValue('ID') when used inside the object.
For field mapping by array ActiveRecord also merges each static::$field for every class from child to parent. Any defined $fields are usable as $Model->$fieldName
.
public static $fields = [
'Tag',
'Slug',
];
By default if you just have a string that will be treated as the name of the field for the model. By default it's treated as a string by PHP and a varchar(255) by the database if allowed to be automatically generated by the framework. There are no default validators so the database will truncate any values above 255 characters.
protected $title; // this will be a varchar(255) treated as a string in PHP
If you try to use a Model and the database responds with the error for "table not found" then it will will automatically build you the SQL to create the table, run it, and rerun the original query without throwing an error to the user.
You can disable this behavior by setting public static $autoCreateTable
to false in your model.
Here's an example of a minimum Model
<?php
namespace yourApp\Models;
class Tag extends \Divergence\Models\Model
{
// support subclassing
public static $rootClass = __CLASS__;
public static $defaultClass = __CLASS__;
public static $subClasses = [__CLASS__];
// ActiveRecord configuration
public static $tableName = 'tags';
public static $singularNoun = 'tag';
public static $pluralNoun = 'tags';
public static $fields = [
'Tag',
];
We get these fields from \Divergence\Models\Model
as defaults.
#[Column(type: "integer", primary:true, autoincrement:true, unsigned:true)]
protected $ID;
#[Column(type: "enum", notnull:true, values:[])]
protected $Class;
#[Column(type: "timestamp", default:'CURRENT_TIMESTAMP')]
protected $Created;
#[Column(type: "integer", notnull:false)]
protected $CreatorID;
Divergence ActiveRecord is super simple and easy making use of native PHP architecture whenever possible.
Example without defaults
$Tag = new Tag();
echo $Tag->Name; // prints null
$Tag->Name = 'Divergence';
echo $Tag->Name; // prints Divergence
Example with record instantiation via construct
$Tag = new Tag([
'Name' => 'Divergence',
]);
echo $Tag->Name; // prints Divergence
Example with record instantiation via create method
$Tag = Tag::create([
'Name' => 'Divergence',
]);
echo $Tag->Name; // prints Divergence
Example with record instantiation via create method and save to database
$Tag = Tag::create([
'Name' => 'Divergence',
],true); // save directly to database right away
echo $Tag->Name; // prints Divergence
echo $Tag->ID; // prints ID assigned by the database auto increment
Another save example
$Tag = new Tag();
$Tag->Name = 'Divergence';
echo $Tag->ID; // prints null
$Tag->save();
echo $Tag->ID; // prints ID assigned by the database auto increment
$Tag = Tag::getByID(1);
echo $Tag->ID; // prints 1
$Tag->Name = 'Divergence';
$Tag->save();
Get By Field
$Tag = Tag::getByField('ID',1);
echo $Tag->ID; // prints 1
$Tag->Name = 'Divergence';
$Tag->save();
$Tag = Tag::getByID(1);
echo $Tag->ID; // prints 1
$Tag->destroy(); // record still in variable
or statically
Tag::delete(1); // returns true if DB::affectedRows > 0
Your model must be defined with a use Versioning
in it's definition.
<?php
namespace Divergence\Tests\MockSite\Models;
use \Divergence\Models\Model;
use \Divergence\Models\Versioning;
class Tag extends Model
{
use Versioning;
You must provide these settings to use versioning.
// versioning
static public $historyTable = 'test_history';
static public $createRevisionOnDestroy = true;
static public $createRevisionOnSave = true;
If you did not create your tables yet a versioned model will have it's history table automatically created.
If you add versioning support after your main table is already in use you must use the SQL class to build yourself a table creation query.
#[Column(type: "integer", unsigned:true, notnull:false)]
protected $RevisionID;
Method | Purpose |
---|---|
getRevisionsByID | Returns an array of versions of a model by ID and $options config. |
getRevisions | Returns an array of versions of a model by $options config. |
Relationship | Type | Purpose |
---|---|---|
History | History | Pulls old versions of this Model |
'History' => [
'type' => 'history',
'order' => ['RevisionID' => 'DESC'],
],
$Model->getByID(1);
$Model->History; // array of revisions where ID == 1 ordered by RevisionID
($Model->History === $Model->History[0]->History) // returns true
Your model must be defined with a use Relationships
in it's definition.
<?php
namespace Divergence\Tests\MockSite\Models;
use \Divergence\Models\Model;
use \Divergence\Models\Relations;
class Tag extends Model
{
use Relations;
For array mapping must provide relationship configurations in the static variable $relationships
.
// relationships
static public $relationships = [
/*
'RelationshipName' => [
.. config ...
]
... more configs
*/
]
Otherwise you can define it with attributes like so.
#[Relation(
type:'one-one',
class:Tag::class,
local: 'ThreadID',
foreign: 'ID',
)]
protected $Tag;
- Relationships should not have the same name.
- The second will override the first.
- Children classes can override parent classes by setting the class configuration to
null
. - Relationship configs will be stacked with priority given to the child class.
- Relationships are callable by their key name from
$this->$relationshipKey
but model field names take priority!
Both of these are actually doing the same thing. Some fields are assumed.
#[Relation(
type:'one-one',
class:Tag::class,
local: 'ThreadID',
foreign: 'ID',
)]
protected $Tag;
#[Relation(
type:'one-one',
class:Post::class,
local: 'PostID',
foreign: 'ID',
)]
protected $Post;
Feel free to create multiple relationship configurations with different conditions and orders.
#[Relation(
type:'one-many',
class:Thread::class,
local: 'ID',
foreign: 'CategoryID'
)]
protected $Threads;
#[Relation(
type:'one-many',
class:Thread::class,
local: 'ID',
foreign: 'CategoryID',
conditions: [
'Created > DATE_SUB(NOW(), INTERVAL 1 HOUR)',
],
order: ['Title'=>'ASC']
)]
Field Type | Database Type | Default Options |
---|---|---|
int | int | notnull = false, unsigned = true, required = false, $default = null |
string | varchar (255) | notnull = false, required = false, $default = null |
float | float | notnull = false, unsigned = true, required = false, $default = null |
enum | enum | values = [], notnull = false, required = false, $default = null |
clob | text | notnull = false, required = false, $default = null |
boolean | int (1) | notnull = false, unsigned = true, required = false, $default = null |
password | varchar (255) | notnull = false, unsigned = true, required = false, $default = null |
timestamp | timestamp | notnull = false, unsigned = true, required = false, $default = null |
date | date | notnull = false, unsigned = true, required = false, $default = null |
serialized | text | notnull = false, unsigned = true, required = false, $default = null |
set | set | notnull = false, unsigned = true, required = false, $default = null |
list | list | notnull = false, unsigned = true, required = false, $default = null |
<?php
namespace App\Models;
use Divergence\Models\Relations;
use Divergence\Models\Versioning;
use Divergence\Models\Mapping\Column;
/*
* The purpose of this Model is to provide an example of every field type
* hopefully in every possible configuration.
*
*/
class Canary extends \Divergence\Models\Model
{
use Versioning;
// support subclassing
public static $rootClass = __CLASS__;
public static $defaultClass = __CLASS__;
public static $subClasses = [__CLASS__];
// ActiveRecord configuration
public static $tableName = 'canaries';
public static $singularNoun = 'canary';
public static $pluralNoun = 'canaries';
// versioning
public static $historyTable = 'canaries_history';
public static $createRevisionOnDestroy = true;
public static $createRevisionOnSave = true;
#[Column(type: 'int', default:7)]
protected $ContextID;
#[Column(type: 'enum', values: [Tag::class], default: Tag::class)]
protected $ContextClass;
#[Column(type: 'clob', notnull:true)]
protected $DNA;
#[Column(type: 'string', required: true, notnull:true)]
protected $Name;
#[Column(type: 'string', blankisnull: true, notnull:false)]
protected $Handle;
#[Column(type: 'boolean', default: true)]
protected $isAlive;
#[Column(type: 'password')]
protected $DNAHash;
#[Column(type: 'timestamp', notnull: false)]
protected $StatusCheckedLast;
#[Column(type: 'serialized')]
protected $SerializedData;
#[Column(type: 'set', values: [
"red",
"pink",
"purple",
"deep-purple",
"indigo",
"blue",
"light-blue",
"cyan",
"teal",
"green",
"light-green",
"lime",
"yellow",
"amber",
"orange",
"deep-orange",
"brown",
"grey",
"blue-grey",
])]
protected $Colors;
#[Column(type: 'list', delimiter: '|')]
protected $EyeColors;
#[Column(type: 'float')]
protected $Height;
#[Column(type: 'int', notnull: false)]
protected $LongestFlightTime;
#[Column(type: 'uint')]
protected $HighestRecordedAltitude;
#[Column(type: 'integer', notnull: true)]
protected $ObservationCount;
#[Column(type: 'date')]
protected $DateOfBirth;
#[Column(type: 'decimal', notnull: false, precision: 5, scale: 2)]
protected $Weight;
public static $indexes = [
'Handle' => [
'fields' => [
'Handle',
],
'unique' => true,
],
'DateOfBirth' => [
'fields' => [
'DateOfBirth',
],
],
];
}
Validation is available to you through a static config in your model. The config is an array of validator configs. Whenever possible Divergence validators will use built in PHP validator filters.
Validators are evaluated in the order in which they appear and validation stops if there's an error. ActiveRecord will throw a simple Exception
. Be aware that setting validators will open you up to Exceptions that you should catch.
// validate
if (!$this->validate($deep)) {
throw new Exception('Cannot save invalid record');
}
Set validators in your model.
public static $validators = [
[
'field' => 'Name',
'required' => true,
'errorMessage' => 'Name is required.',
],
];
[
'field' => 'Name',
'minlength' => 2,
'required' => true,
'errorMessage' => 'Name is required.',
]
[
'field' => 'Name',
'maxlength' => 5,
'required' => true,
'errorMessage' => 'Name is too big. Max 5 characters.',
]
[
'field' => 'ID',
'required' => true,
'validator' => 'number',
'max' => PHP_INT_MAX,
'min' => 1,
'errorMessage' => 'ID must be between 0 and PHP_INT_MAX ('.PHP_INT_MAX.')',
]
[
'field' => 'Float',
'required' => true,
'validator' => 'number',
'max' => 0.759,
'min' => 0.128,
'errorMessage' => 'ID must be between 0.127 and 0.760',
]
Email Validation
[
'field' => 'Email',
'required' => true,
'validator' => 'email',
]
Custom Validation
[
'field' => 'Email',
'required' => true,
'validator' => [
Validate::class, // this is the actual Validate class that comes with Divergence
'email', // so look at this method for an example for how to make your own validator
],
]
Every ActiveRecord save will call $class::$beforeSave
and $class::$afterSave
if they are set to PHP callables.
If you set ActiveRecord::$beforeSave
you can hook into every save for every model on the entire site.
Both $beforeSave
and $afterSave
get passed an instance of the object being saved as the only parameter.
Events are not overriden by child classes. An event will fire for every parent of a child class.
foreach (static::$_classBeforeSave as $beforeSave) {
if (is_callable($beforeSave)) {
$beforeSave($this);
}
}
foreach (static::$_classAfterSave as $afterSave) {
if (is_callable($afterSave)) {
$afterSave($this);
}
}
Please look at ActiveRecord::_defineEvents()
to see how ActiveRecord builds the event chain.
Please also note that if validation failed $afterSave
will never fire.
Here I'll show a few examples of how to use ActiveRecord but still do some custom things with your model.
This is a case where you'll want to extend getValue($field)
.
Here I'm showing different ways of doing it for different reasons.
public getValue($field) {
switch($field) {
case 'HeightCM':
return static::inchesToCM($this->Height);
case 'calculateTax':
return $this->calculateTaxTotal();
default:
return parent::getValue($field);
}
}
public static function inchesToCM($value)
{
return $value * 2.54;
}
public function calculateTaxTotal() {
$taxTotal = 0;
if($state = $this->getStateTaxRate()) {
$taxTotal += ($state * $this->Price);
}
if($local = $this->getLocalTaxRate()) {
$taxTotal += ($local * $this->Price);
}
return $taxTotal;
}
In this example we let the table names come right from the class. One thing less to remember. We also make sure our query only gives us the one model we actually want to instantiate from the data. This way we can do complex conditions on other tables in relation to the model we care about. Here we are pulling all BlogPost objects by TagID.
if (App::$App->is_loggedin()) {
$where = "`Status` IN ('Draft','Published')";
} else {
$where = "`Status` IN ('Published')";
}
$BlogPosts = BlogPost::getAllByQuery(
"SELECT `bp`.* FROM `%s` `bp`
INNER JOIN %s as `t` ON `t`.`BlogPostID`=`bp`.`ID`
WHERE `t`.`TagID`='%s' AND $where",
[
BlogPost::$tableName,
PostTags::$tableName,
$Tag->ID,
]
);
public getValue($field) {
switch($field) {
case 'getAllByTag':
return static::getAllByTag($_REQUEST['tag']);
default:
return parent::getValue($field);
}
}
public static function getAllByTag($slug) {
if($Tag = Tag::getByField('Slug', $slug)) {
if (App::$App->is_loggedin()) {
$where = "`Status` IN ('Draft','Published')";
} else {
$where = "`Status` IN ('Published')";
}
return static::getAllByQuery(
"SELECT `bp`.* FROM `%s` `bp`
INNER JOIN %s as `t` ON `t`.`BlogPostID`=`bp`.`ID`
WHERE `t`.`TagID`='%s' AND $where",
[
static::$tableName,
PostTags::$tableName,
$Tag->ID,
]
);
} // if
} // getAllByTag