Skip to content
macik edited this page Dec 29, 2014 · 55 revisions

CotORM

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().

Table of contents

Introduction

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.

Module root file

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.

Models

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:

Column 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.
Special column types
  • 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.
Special values for on_insert and on_update
  • 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.

Controllers

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.

Creating a project

<?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.

Finding and listing projects

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...
    }
}

?>

Automatic import

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
}

Module setup

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.