-
Notifications
You must be signed in to change notification settings - Fork 2
CotORM is a basic ORM tool for the Cotonti CMF. It consists of only one
file with one abstract class. It's intended to be placed inside the Cotonti
system folder, so it can be easily included in modules using cot_incfile()
.
CotORM follows the MVC pattern, but doesn't force you to do so. While combining it with the MVC helper is the most natural choice, CotORM also allows you to write your functional code in the classic, procedural way which is common in most Cotonti modules and plugins. However, the examples below assume you're using the MVC helper. Here's an overview of file locations:
Classic | MVC | |
---|---|---|
Models | modules/issuetracker/classes | modules/issuetracker/models |
Controllers | modules/issuetracker/inc | modules/issuetracker/controllers |
Views | modules/issuetracker/tpl | modules/issuetracker/views |
By method of documentation, we'll be implementing a module named 'issuetracker'. A simple issue tracker could contain Projects, Milestones and Issues. The code fragments below show how one could implement the Projects part of the module.
Any incoming request to a Cotonti module (e.g. index.php?e=issuetracker) starts in the module's root file (modules/issuetracker/issuetracker.php in this case). In the classic Cotonti scenario, you'd use this file to include the correct 'inc' file (usually based on the value of $m), which contains code to to something based on the URL parameters (usually $a and others). Here's what the file might look like in this classic scenario:
modules/issuetracker/issuetracker.php:
<?php
/* ====================
[BEGIN_COT_EXT]
Hooks=module
[END_COT_EXT]
==================== */
defined('COT_CODE') or die('Wrong URL.');
require_once cot_incfile('orm');
if (file_exists("{$cfg['modules_dir']}/{$env['ext']}/inc/{$env['ext']}.$m.php"))
{
require_once "{$cfg['modules_dir']}/{$env['ext']}/inc/{$env['ext']}.$m.php";
}
?>
In the case of MVC, we can use the mvc_dispatch()
function to automatically
handle incoming requests and call a certain function in the controller. For
example, a GET request to e=issuetracker&m=project&a=list would call the
ProjectController::get_list()
method or project_get_list()
function in
controllers/project.php, depending on the method of implementation
(object-oriented or procedural). A POST request to the same URL would call
ProjectController::post_list()
or project_post_list()
. Here's an example
of the object-oriented version:
<?php
/* ====================
[BEGIN_COT_EXT]
Hooks=module
[END_COT_EXT]
==================== */
defined('COT_CODE') or die('Wrong URL.');
require_once cot_incfile('orm');
require_once cot_incfile('mvc');
mvc_dispatch($m, $a) or cot_die_message(404);
?>
Even though the difference in lines of code isn't that great, using this style enforces a clear separation of controller code into functions or methods, effectively making your code a lot more readable and maintainable. The code examples below assume you'll be using the MVC style of writing controllers.
One of the main advantages of using an ORM is the central definition of models, which are used by the ORM to perform many automated tasks. One could argue that a good model definition is the most important part of your module. Assuming you're building something that relies heavily on database storage, of course.
Defining your models is very important because it's the central place for configuring CotORM's behavior. If you do not configure it correctly, you will leave security holes and possibly introduce bugs. In a worst-case scenario, you may even lose your data, although that's very unlikely. Make sure you utilise properties such as 'hidden' and 'locked' where appropriate.
As indicated above, your module should have a folder named 'models', in which you will store your model classes. If you prefer not to use the MVC helper, you can store your models in a folder named 'classes' instead. Cotonti's built-in autoloader will already look for files there.
modules/issuetracker/models/Project.php:
<?php
defined('COT_CODE') or die('Wrong URL.');
class Project extends CotORM
{
protected static $table_name = 'projects';
protected static $columns = array(
'id' => array(
'type' => 'int',
'primary_key' => true,
'auto_increment' => true,
'locked' => true // An ID can never change
),
'ownerid' => array(
'type' => 'int',
'foreign_key' => 'users:user_id', // Verifies that the provided owner ID exists in the users table
'locked' => true // Changing project ownership is not allowed
),
'name' => array(
'type' => 'varchar',
'maxlength' => 50,
'unique' => true // project name must be unique
),
'metadata' => array(
'type' => 'object' // This can be used to store arbitrary data related to the project
),
'state' => array(
'type' => 'enum',
'options' => array('pending', 'active', 'closed') // A project is always in one of these states
),
'created' => array(
'type' => 'int',
'on_insert' => 'NOW()', // Sets current timestamp when project is created
'locked' => true // Can't be changed afterwards
),
'updated' => array(
'type' => 'int',
'on_insert' => 'NOW()',
'on_update' => 'NOW()', // Sets current timestamp when project is updated
'locked' => true // Can't be changed by the user
)
);
}
?>
Because the model class extends CotORM, it will inherit all of CotORM's
methods and properties, which of course you can override if you need to.
Since CotORM contains all the fancy methods, all we need to do here is
configure the properties of Project. There are two properties we have to
configure: $table_name
and $columns
.
$table_name
is the name of the database table to store Project objects.
A common convention is to use the lowercase, plural of the class name, so
'projects' in this case. CotORM will automatically prepend Cotonti's $db_x
to the table name.
$columns
is where things get complicated. It is where you configure the
database columns for the objects. CotORM will automatically validate incoming
data based on the rules set in $columns. This includes variable type checking,
foreign key constraints and unique values. It also allows you to 'lock' and/or
'hide' a column from the outside world. Here's an overview of allowed properties:
Property | Datatype | Default | Description |
---|---|---|---|
type | string | - | Always required. MySQL data type (lowercase), such as 'int', 'varchar', 'text' or one of the special values such as 'enum' (see below). |
locked | bool | false | Disallows UPDATE queries on this column. |
hidden | bool | false | Makes the column not appear in result objects. |
required | bool | false | Flag the field as required. Will accept NULL if 'nullable' => true. |
minlength | int | 0 | Minimum string length of the value. |
maxlength | int, string | varchar: 255, others: - | Maximum display length of the value, or in case of float or decimal, a string representing precision and scale (see MySQL manual). |
nullable | bool | false | If true, the MySQL column will accept NULL values and 'required' and 'minlength' flags will be ignored if the passed value is NULL. Also, default_value will be ignored on update if NULL is passed and 'enum' columns will accept NULL aside from their regular options. |
signed | bool | false | Flag the field as signed (allow negative values). For numeric types only. Unsigned is used by default. |
alphanumeric | bool | false | Enforces values to be alphanumeric. |
primary_key | bool | false | If true, the column will be considered the primary key and be used as object identifier. |
foreign_key | string | - | Table and column name pair which the column is directly related to. CotORM will enforce the foreign key dependency. Table and column name must be seperated with a colon. Table name should not include `$db_x`. Example: 'users:user_id' |
index | bool | false | If true, sets an MySQL INDEX on this column. |
unique | bool | false | If true, sets an MySQL UNIQUE constraint on this column. |
auto_increment | bool | false | If true, sets the MySQL AUTO_INCREMENT flag on this column. |
default_value | string, int, float | - | MySQL DEFAULT value. If foreign_key is given, this is the only value which will pass even if such a foreign record doesn't exist. |
on_insert | string, int, float | - | Default value for the column in INSERT queries. Accepts several special values, see the listing below. |
on_update | string, int, float | - | Default value in UPDATE queries. Also accepts special values. |
options | array | - | Required for 'enum' columns. Numeric array of allowed values. The first option becomes the default_value, unless another default_value is defined. |
- object: Allows storage of PHP objects. Data will automatically be serialized/unserialized and stored as text.
- enum: Accepts values in a predefined set of options. Allowed values must be defined in the 'options' property as a numeric array. Will also accept NULL if 'nullable' => true.
- NOW() => current UNIX time (integer)
- RANDOM() => random alphanumeric string or integer of maxlength length.
- INC() => Increase by one ($value++). Sets 0 on_insert.
- DEC() => Decrease by one ($value--). Sets 0 on_insert.
The controller is where your business logic goes. This is the layer between the database (models) and template output (views). The actual code that goes in the controller differs greatly between modules, as this is where you put the code that makes the module behave in the way it does. Note that we're using the MVC style of coding here, which means *_index is called when no value for $a is provided.
<?php
defined('COT_CODE') or die('Wrong URL.');
class ProjectController
{
/**
* Create new project
*/
public function post_index()
{
$name = cot_import('name', 'P', 'TXT', 50);
$desc = cot_import('desc', 'P', 'TXT');
if ($name && $type)
{
$obj = new Project(array(
'name' => $name,
'metadata' => array(
'description' => $desc
),
'ownerid' => $usr['id']
));
if ($obj->insert())
{
// succesfully added to database
}
}
}
}
?>
The insert()
and update()
methods are wrappers for a more generic function
called save()
. This method can take one argument, which can either be 'insert'
or 'update'. If you don't pass this argument it will try to update an existing
record and if that fails try to insert a new record. The save() method has 3
possible return values: 'added', 'updated' or null. insert()
and update()
return a boolean.
To get existing objects from the database, CotORM provides three
'finder methods'. These basically run a SELECT query on the database and return
rows as objects of the type the finder method was executed on. The three
variants are find()
, findAll()
and findByPk()
, which respectively will
return an array of objects matching a set of conditions, return an array of all
objects or return a single object matching a specific primary key.
Here's an example use case, listing all projects and assigning data columns to template tags:
<?php
defined('COT_CODE') or die('Wrong URL.');
class ProjectController
{
/**
* List all projects
*/
public function get_list()
{
global $env;
list($page, $offset, $urlnum) = cot_import_pagenav('p', $cfg['issuetracker']['projectsperpage']);
$totalcount = Project::count();
$totalcount && $projects = Project::findAll($cfg['issuetracker']['projectsperpage'], $offset, 'name');
if ($projects)
{
foreach ($projects as $project)
{
foreach ($project->data() as $key => $value)
{
$t->assign(strtoupper($key), $value, 'PROJECT_');
}
$t->parse('MAIN.PROJECTS.ROW');
}
$pagenav = cot_pagenav($env['ext'], parse_str($_SERVER['QUERY_STRING']), $page, $totalcount, $cfg['issuetracker']['projectsperpage']);
foreach ($pagenav as $key => $value)
{
$t->assign(strtoupper($key), $value, 'PAGENAV_');
}
$t->parse('MAIN.PROJECTS');
}
// etc...
}
}
?>
This is convenient for lists, but what about a details page of a specific object? Here's how to do that:
<?php
defined('COT_CODE') or die('Wrong URL.');
class ProjectController
{
/**
* Show project
*/
public function get_index()
{
$id = cot_import('id', 'G', 'INT');
$project = Project::findByPk($id);
foreach ($project->data() as $key => $value)
{
$t->assign(strtoupper($key), $value, 'PROJECT_');
}
// etc...
}
}
?>
It is not necessary to import each property of an object separately from form input, with CotORM it can be done at once:
$obj = Project::import();
if ($obj->insert())
{
// succesfully added to database
}
CotORM provides a way to simplify the install and uninstall files of your
module. It has two useful methods for setup, createTable()
and dropTable()
.
createTable()
will create the table based on the configuration provided in the
model. For example, issuetracker.install.php file may look like this:
<?php
defined('COT_CODE') or die('Wrong URL.');
require_once cot_incfile('orm');
Project::createTable();
Milestone::createTable();
Issue::createTable();
?>
issuetracker.uninstall.php will look similar, except that it should call
dropTable()
instead of createTable()
. Of course you might choose not to drop
the tables upon uninstallation, but that's your choice as a developer.