ORM System

The ORM system acts such as a data provider, directly at the SQL level.
You don't need anymore to write SQL queries manually.

'ORM' stand for Object-Relational Mapping.
It basically means that your component is able to link tables together automatically.

For purists... I know, this is not yet a complete ORM system, but it has the foundations to introduce such new way for loading datas. And more in future!



Ask it, Get it

Simply ask what you want to the model.
Example:

$model->orm(array(
    'select' => array(
        'name', 'population'
    ),
    'order' => array(
        'name' => 'ASC'
    )
));
$items = $model->getItems();

Headless : Helper::getData()

Get any data anywhere, using the helper of your component:

$cities = XxxxHelper::getData('cities', array(
    'select' => array(
        'name', 'population'
    ),
));

And for ITEM model :

$city = XxxxHelper::getData('city', array(
    'select' => 'name', 'population'
), 3); // Load the city #3

Structure

All ORM instructions can be sent trough one single array, or can be decomposed in successive calls:

$model->orm(array(
    'select' => array(
        'name', 'population'
    ),
));

$model->orm(array(
    'order' => array(
        'name' => 'ASC'
    )
));
$items = $model->getItems();

It is also possible to call the instructions direcly to the ORM object, available in the model.

$this->orm->select(array(
    'name', 'population'
));

$this->orm->order(array(
    'name' => 'ASC'
));

Source code

The ORM system acts such as a helper, but is is instanced in the model.
It can be reused for other Joomla! components, but it requires some proxies of the model :

  • addSelect()
  • addWhere()
  • addOrder()
  • addGroupOrder()
  • addJoin()
  • addSearch()
  • ...

Thoses proxies are preventing SQL statements to be doubled, which would end with SQL error.
They could eventually be moved into the ORM system, but they are still used independently by your components, outside of the ORM System.



ORM directives

SELECT

Choose the columns to load

$model->orm(array(
    'select' => array(
          'name'
        , 'population'
    ),
));

Namespaces (Foreign Keys)

Don't worry anymore about building the foreign key relations for loading foreign values.
Simply ask the foreign columns with namespaces.

$model->orm(array(
    'select' => array(
        'country.name'
    ),
));

Aliases

Define a var name for every required field.

$model->orm(array(
    'select' => array(

        // Rename a field
        // SQL : SELECT `name` AS title
        'name' => 'title',

        // Works also with namespaces
        // SQL : SELECT `_country_.name` AS countryName
        'country.name' => 'countryName',

    ),
));

Alternative syntax

Comma separated

$model->orm(array(
    'select' => 'name, population,country.name'
));

From the model (using ORM object):

$this->orm->select('name, population,country.name');




CONTEXT

Set the model context.
A context is a namespace. (words seperated with points)

ex : 'layout.default'

The context is used to group the state vars into profiles.
They are used, for instance, for storing the values of user's permanent filters separately, and so... avoiding conflicts with any other call over the same model.

$model->orm(array(
    'context' => 'layout.default'
));

Alternative syntax from the model (using ORM object):

$this->orm->context('layout.default');

Load a predefined query

For every layout you have created, the builder will create a new unique state profile, in wich will be defined all the ORM description.

In the model, you will find the functions :

ormLayout[layoutName]()
{

}

You only need to fork those functions for your customs.
They contains all the ORM instructions for every context.

You can create your own context functions:
They are camelCase formated (as always), and contains the context namespace with 'orm' prefix.
ex: 'ajax.custom'

ormAjaxCustom()
{

}




ID

The 'id' directive is used to select a unique record for the ITEM model.
By default, Joomla! is loading an item from the given primary key value. Now with ORM, you are free to search differently.

When you are querying an item, you MUST define this directive.

Native Primary Key

$model->orm(array(
    'id' => array(
        'id' => 'pk'
    )
));

Alternative search

$model->orm(array(
    'id' => array(
        '[field1]' => [value1],
        '[field2]' => [value2],
    )
));

Example for a pivot table (Visits)

$model->orm(array(
    'id' => array(
        'city.alias' => 'paris',
        'traveller.name' => 'Chris',
    )
));

Only the first result will be returned.

Alternative syntax

Short version for primary key

$model->orm(array(
    'id' => 'pk'
));

From the model (using ORM object):

$this->orm->id('pk');




FILTER

The filter directive is able to:

  • Handle different filters types
  • Automatically join the requirements (Foreign Keys)
  • Get the filter value from the model state var (when defined)
  • Force a filter value from the caller, when the ORM system is called headless.
$model->orm(array(
    'filter' => array(
        '[filterName]' => array(
            'type' => '[filterType]',           // Mandatory
            'format' => '[fieldFormat]',        // Field format, Optional
            'namespace' => '[fieldNamespace]',  // When the filtered field is accross Foreign Keys (Very important)
            'value' => [customValue],           // Force the filter value (otherwise, the value is read from the filter state var)
            ...
        )
    ),
));

Alternative syntax

From the model (using ORM object):

Example with 'Pivot' type

$this->orm->filter('[filterName]', array(
    'type' => 'pivot',
    ...
));

Or shorter, using the filter type function directly:

$this->orm->filterPivot('[filterName]', array(
    ...
));


Value

(string / integer / FK / enumeration / datetime)

Search for exact match of a value.

$model->orm(array(
    'filter' => array(
        '[filterName]' => array(
            'type' => 'string', // Facultative because 'string' is the default type
            ...
        )
    ),
));

Or alternatively :

$this->orm->filterString('[filterName]', array(
    ...
));


Range

(integer / datetime)

Search for a value contained between 2 values (included)

$model->orm(array(
    'filter' => array(
        '[filterName]' => array(
            'type' => 'range',
            'from' => [valueFrom],  // Force value From
            'to' => [valueTo],      // Force value To

            ...
        )
    ),
));

Or alternatively :

$this->orm->filterRange('[filterName]', array(
    ...
));


Period

(datetime,date,time)

Search for a date included in a period.
A period is a date range expressed in human language.

$model->orm(array(
    'filter' => array(
        '[filterName]' => array(
            'type' => 'period',
            'value' => [periodValue],   // Force period value
            'range' => [rangeValue],    // Force range value

            ...
        )
    ),
));

Or alternatively :

$this->orm->filterPeriod('[filterName]', array(
    ...
));

[periodValue] : Define a common range, or define the begining of the range, relative to the actual date.
Accepted values :
* null | defined -> Check the presence of the date * today | now -> Base the range around the actual date * +1day | -3hour | +1month ... -> time span string


[rangeValue] : Defines the duration of the range, relatively to the begining of the range
Accepted values :
* +1day | -3hour | +1month ... -> time span string



This filter is not available from the builder, but you can use it manually from the ORM system.


Multi

(integer / FK / enumeration / datetime)

Search between a set of possible values

$model->orm(array(
    'filter' => array(
        '[filterName]' => array(
            'type' => 'multi',
            'format' => '[fieldFormat]',
            'logic' => '[AND|NOT]'
            'value' => array([values]),      // For Multi, the values are furnished in array
            ...
        )
    ),
));

Or alternatively :

$this->orm->filterMulti('[filterName]', array(
    ...
));


Pivot (N:m)

(string / integer / FK / enumeration / datetime)

Search on a cross-reference table

$model->orm(array(
    'filter' => array(
        '[relationName]' => array(              // Filter name
            'type' => 'pivot',
            'logic' => '[AND|OR|NOT]',
            'value' => array([values]),         // For Pivot, the values are furnished in array
            'relation' => '[relationName]'      // Optional. When the name of the relation is different than the filter name
            ...
        )
    ),
));

Or alternatively :

$this->orm->filterPivot('[relationName]', array(
    ...
));




SEARCH

Processes a custom text search over multiple fields.

$model->orm(array(
    'search' => array(
        '[searchName]' => array(
            'on' => array(
                '[fieldName]' => 'like',    // Contains the string
                '[fieldName]' => 'exact',   // Exact value
            ),
            'logic' => 'AND',       // Concatenation of the conditions. Default: AND. Can be AND | OR
            'ignoredLength' => 3,   // Optional. All words inferiors of equals to 3 will be ignored
        )
    )
));

Or alternatively :

$this->orm->search('[searchName]', array(
    ...
));




RELATION

Load the N:m lists of your object.

$model->orm(array(
    'relation' => array(
        'travellers' => array(
            // ORM Description
             'select' => 'name,photo'
        )
    )
));

Nested queries

The relation configuration is a sub query ORM description. It accepts the same features as the main object.
This way you can build unlimited nested queries. Example :

$model->orm(array(
    'relation' => array(


        // Nested ORM (Travellers)
        'travellers' => array(
            'select' => 'name,photo',
            'filter' => array(),
            'relation' => array(



                // Nested ORM (Cities)
                'cities' => array(
                    'select' => 'name,population',
                    'order' => array(),
                    'pagination' => array(),

                )
            )
        )
    )
));

How it works

In this case, the ORM system do not load the related items in the same SQL base query.
When items of the base query are known, a second SQL query stand for completing the instanced items. -> populateObjects()
With the 'RELATION' instruction , the ORM system will simply store the ORM description into a model state var called:
'relation.[relationName]'

Then the model will load its objects basing of those ORM states.
This process work in unlimited cascade trough the involved models.

Alternative syntax

From the model (using ORM object):

$this->orm->relation('travellers', array(...);




ACCESS

Protect the datas using 3 different methods:

  • Accesslevel
  • Published state
  • Authoring
$model->orm(array(
    'access' => array(
       '[table]' => array(
            '[accessType]' => '[accessField]',
            '[accessType]' => '[accessField]',
            '[accessType]' => '[accessField]',
       )
    )
));

[table] : Table namespace (Not the table name):

  • Root table is called 'a'
  • Foreign tables are called by their Foreign Key namespaces


[accessType] can be :

  • publish
  • access
  • author


[accessField] is the name of the field containing the access

This example shows how to apply the published state + authoring to the root table ('a')

$model->orm(array(
    'access' => array(
       'a' => array(
            'publish' => 'published',
            'author' => 'created_by'
       )
    )
));

Propagate accesses

When a Foreign table is containing accesses that you need to propagate to the root table, simply say it that way :

$model->orm(array(
    'access' => array(
       'country' => array(  // Here 'country' is a Foreign Key field, for wich the accesses will propagate
            ...
       )
    )
));

Same example with a 2nd level depth of Foreign Table

$model->orm(array(
    'access' => array(
       'country.continent' => array(
            ...
       )
    )
));

Alternative syntax

From the model (using ORM object):

$this->orm->access('a', array(...);




ORDER

Order a list.

$model->orm(array(
    'order' => array(
       'country.name' => 'ASC',
       'name' => 'ASC',
    )
));

Alternative syntax

From the model (using ORM object):

$this->orm->order(array(...);




GROUP ORDER

Prioritary ORDER statement for grouping lists over Foreign Key values

$model->orm(array(
    'groupOrder' => array(
       'country.continent.name' => 'ASC',
       'country.name' => 'ASC',
    )
));

Alternative syntax

From the model (using ORM object):

$this->orm->groupOrder(array(...);




PAGINATION

Native Joomla! pagination system

  • Defines the LIMIT of returned results
  • Defines the item number in list from wich to START
$model->orm(array(
    'pagination' => array(
       'limit' => 20,
       'start' => 0
    )
));

Alternative syntax

From the model (using ORM object):

$this->orm->pagination(array(...);



Roadmap


VERSION

Versioning system.
Not available yet.

This directive will be used for managing versions such as :

  • Language translations
  • Time machine versioning
  • User versioning


I wrote this versioning system 3 years ago, but I never implemented in Cook because I was not sure of how to present the translation interface, using the less code possible and HTML.
For this first version of ORM, I even didn't looked closer again to that, but I will implement in future.


TREE

Hierarchical trees system.
Not available yet.

I don't use the trees so much, and that's why I never digged in personally.
Joomla and FOF offers that model behaviors, and Cook will do too.

This is a regrettable miss at the moment.




Suggestions

If you like to work with the ORM system, share your feedbacks.
Once tested and completed, this magic class will be submited to the Joomla! core.
Just a question of time.


"The road is long, but the way is free"
[Framasoft]

Get Started