Generic o/r mapper

1. Introduction

Designing object oriented applications, you typically claim, that the complete design can be created on OO style - without break of media. In order to achieve this goal, each developer was yet faced with the problem of data storage in relation databases. If there is no tool, a DataMapper has to be implemented newly for each application. This does not only result in higher costs and lack of time, but is also not compatible with the DRY (don't repeat yourself) paradigm. You still are forced to produce redundant code.

The APF module genericormapper now provides an abstraction layer, that is intended to do most of the work a data mapper has to do. The functionality thereby can

  • manage objects,
  • manage relations between objects (compositions and associations) and
  • features CRUD functionality for objects and object structures (aka. trees).

For this tasks, the API features a couple of methods, that support loading, manipulation and deletion of objects within the database. The generic domain object (GenericDomainObject) returned by the API functions can on the one hand be used directly within your applications or your application features a simple data layer, that mapps the data mapper domain objects into your application's domain objects and vice versa.

The following chapters show how the o/r mapper has to be configured and how you can use it. The module usermanagement shipped with the APF release is completely based on the GenericORMapper and thus can be used as an extended implementation example. Details on the module can be seen on the User management module page.

2. Configuration of the o/r mapper

2.1. Basics

To be able to user the o/r mapper, two configuration files must be present:

  • {ENVIRONMENT}_{NAMEAFFIX}_objects.ini
  • {ENVIRONMENT}_{NAMEAFFIX}_relations.ini

The first file defines the objects and attributes, the second one is intended to define the relations between the objects from the first file. Due to the fact, that the GenericORRelationMapper uses the ConnectionManager for database access, a database connection configuration must be created if not done yet.

The {ENVIRONMENT} part of the configuration file names is taken from the registry directive Environment from the APF\core namespace. The {NAMEAFFIX} can be defined freely by the developer. This part of the name is inteded to provide the possibility of different mapper configurations for one context and one environment. This enables you to create applications, that are able to use multiple data sources.

2.2. Configuration examples

Imagine, that a developer wants to create a guestbook. The source code files are located in the VENDOR\modules\myguestbook namespace and the guestbook's data layer should us the o/r mapper. Further, the registry value Environment is not touched (so it is still the default value) and the current application context is sites\mysite. Moreover, he decides, that the name affix should be identical to guestbook. In this case, the object configuration file must be named

Code
DEFAULT_guestbook_objects.ini

and the relation configuration file

Code
DEFAULT_guestbook_relations.ini

These must reside within the folder

Code
/APF/config/modules/myguestbook/sites/mysite

Further details on configuration, namespaces and contexts can be taken from the configuration scheme definition.

2.3. Object and relation definition

The way of object and relation definition is described in the next two chapters:

2.3.1. Object definition

The GenericORRelationMapper provides a generic domain object (GenericDomainObject) that represents a persistence object. The type of the concrete object is classified by the domain object's ObjectName property.

The object definition thus contains the name of the object (=name of the section) and the attributes (=properties of the GenericDomainObject class). The following codebox shows a typical object configuration:

APF configuration
[Application] DisplayName = "VARCHAR(100)" [User] DisplayName = "VARCHAR(100)" FirstName = "VARCHAR(100)" LastName = "VARCHAR(100)" EMail = "VARCHAR(100)" Username = "VARCHAR(100)" Password = "VARCHAR(100)" [Group] DisplayName = "VARCHAR(100)" [Role] DisplayName = "VARCHAR(100)"

The values of the attributes represent the database data types of the object's properties. The mapper knows the following values and translates them into the right create table statements:

  • VARCHAR({LENGTH})
  • TEXT
  • DATE

The {LENGTH} place holder must be replaced by any number. All other values are directly used for the data type of a property. These must be valid data type definitions. Otherwise, the CREATE TABLE statements will fail. The values presented above should be suitable for most application cases.

The generic or mapper also supports bit masking. For this reason, the value of the property definition must contain a valid BIT field definition like
SQL statement
bit(7) NOT NULL default b'0'
Thereby, it is not relevant, if the property is defined with a default value. But it is important that the declaration of the BIT field contains the keyword "BIT". Details can be taken from the forum discussion under Fehler mit BIT-Feldern (German).

The attributes of a domain object can then be addressed as follows:

PHP code
... $user = new GenericDomainObject('User'); $user->setProperty('FirstName', 'Christian'); $user->setProperty('LastName', 'Achatz'); ... echo 'Vorname: ' . $user->getProperty('FirstName'); echo 'Name: ' . $user->getProperty('LastName'); ...
2.3.2. Relation definition

The file *_relations.ini defines the relations between the objects defined in the previous chapter. The mapper knows two types of relations: compositions and associations. Due to the fact, that compositions are strong relations, objects, that compose other objects, cannot be deleted due to data consistency. This case is checked by the mapper and it throws an error, if a object is tried to be deleted, that must not.

Note: The persistancy theory contains the rule, that each object should be composed exactly one time. This is because an object cannot have more than one strong binding to another object in real life, too. Futhermore, a composition relation defines the object's right to exist. Hence, be aware that your objects are composed concerning this rule. For example, a guestbook entry cannot exist without the guestbook object. Instead, a user can exist without the guestbook. This means, that the relation between a guestbook and an entry must be a composition, the relation of the entry to the user must be an association.

The following codebox shows a typical relation configuration:

APF configuration
[Application2Group] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "Group" [Group2User] Type = "ASSOCIATION" SourceObject = "Group" TargetObject = "User" [Role2User] Type = "ASSOCIATION" SourceObject = "Role" TargetObject = "User" [Application2User] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "User" [Application2Role] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "Role"

The section name (e.g. Group2User) should be named self-explanatory, because this relation key can be used to load objects, that are related to others and to create a relation between two or more objects. The type attribute contains the quality of relation, the SourceObject and TargetObject params reference the relevant object definition sections of the object definition file.

You can also use self-references, which means that an object can reference another object of the same object type. There is no difference to normal references, you just configure the same name in source- and target object:

APF configuration
[User2BlockedUser] Type = "ASSOCIATION" SourceObject = "User" TargetObject = "User"

Also, when using this feature, you do not take care of anything, as long as the SQL-statements get generated from the methods of the o/r mapper, and do not get written on your own.

Note:

  • The count of the relation definitions is not limited, each definition should be created to fit the requirements of the data model of the application. The rule of thumb here says, that when using one attribute more than one time in different objects, it should be outsourced to an independent object and referenced (associated) by the relevant objects. A typical example is the language.
  • If you have created an object tree using the method addRelatedObject(), you can read the related objects of the desired node (GenericDomainObject) within the tree using getRelatedObjects() for further manipulation or processing.

2.4. Additional indices

It is possible to define additional indices along with the object definition. This may be necessary due to performance reasons and it is strongly recommended to use this feature, if you are heavily requesting data using index restriction.

In such cases an object definition can be added the AddIndices key. The information provided there is used during setup and update to create additional indices to speed up the query execution time.

The subsequent code box describes a simple example with three types of indices defined within a user's object definition:

APF configuration
[User] ... FirstName = "VARCHAR(100)" LastName = "VARCHAR(100)" Username = "VARCHAR(100)" Password = "VARCHAR(100)" ... AddIndices = "FirstName,LastName(INDEX)|Username(UNIQUE)|Password(INDEX)"

The additional index definition underlies the following scheme:

  • Each additional index is defined by the included properties and the index type. Multiple definitions are separated by "|" (Pipe).
  • Valid index types are: INDEX (normal index), UNIQUE (property may only contain a unique value), And FULLTEXT (search index, that can be searched using a MATCH AGAINST() statement). All index types are noted in brackets.
  • In case multiple properties should be included in one index definition, the properties must be separated by colon (","). The names of the properties must be equal to the names used for the usual object definition.
Details on the discussion of this feature can be taken from the wiki page Zusätzliche Indizes für Setup-/Update-Tool GORM (German) and the forum thread Zusätzliche Indizes für Setup-/Update-Tool GORM (German).

2.6 Set Storage Engine per Table

Since Version 2.1 it's possible to define the Storage Engine along with the relation or object definition. To do that you must add the key StorageEngine and set it to the desired storage engine name. The information provided there is used during setup and update with the GenericORMapperManagementTool to set the engine for this table. The subsequent code boxes describe two examples for setting up the storage engine for a object or a relation table

APF configuration
[{relation-name}] Type = "ASSOCIATION|COMPOSITION" SourceObject = "{source-table}" TargetObject = "{source-table}" StorageEngine = "MyISAM"
APF configuration
[User] ... FirstName = "VARCHAR(100)" LastName = "VARCHAR(100)" Username = "VARCHAR(100)" Password = "VARCHAR(100)" ... StorageEngine = "InnoDB"

If the key StorageEngine is not presented the GenericORMapperManagementTool will set the storage engine to MyISAM or the default engine set with the setStorageEngine method.

2.5. Creation date of relations

Creation dates of relations can be stored within the data model. In order to activate the feature the relation configuration must include the Timestamps attribute set to TRUE.

APF configuration
[{relation-name}] Type = "ASSOCIATION|COMPOSITION" SourceObject = "{source-table}" TargetObject = "{source-table}" Timestamps = "TRUE"
Please be aware to activate the feature for each relation definition separately.

Afterwards, the data model can be created or updated by using the GenericORMapperManagementTool.

Please note, that missing values for existing relations are not created with an update of the data model. Asking for the relation creation date you will receive null as it's value.

The usage of this feature is described in chapter 4.5.

3. Database management

3.1. Database setup

After the configuration files have been created, the database must be prepared for usage. For this reason, the GORM ships the GenericORMapperManagementTool. With this class databases can be created or updated.

The following script shows, how the automated database setup can be done with aim of the GenericORMapperManagementTool tool. If you are searching for an example script, have a look at the /apps/modules/genericormapper/data/tools folder in the apf-codepack-* release. The file is named setup.php.

In order to use the template, it must be adapted to fit your use case according to the notes below. The script is as follows:

PHP code
// include APF bootstrap file require('./APF/core/bootstrap.php'); // configure the registry if desired use APF\core\registry\Registry; Registry::register('APF\core', 'Environment', '{ENVIRONMENT}'); use APF\modules\genericormapper\data\tools\GenericORMapperManagementTool; $setup = new GenericORMapperManagementTool(); // set Context (important for the configuration files!) $setup->setContext('{CONTEXT}'); // adapt storage engine (default is MyISAM) //$setup->setStorageEngine('MyISAM|INNODB'); // adapt data type of the indexed columns, that are used for object ids //$setup->setIndexColumnDataType('INT(5) UNSIGNED'); // initialize mapping configuration $setup->addMappingConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // initialize relation configuration $setup->addRelationConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // initialize database connection (optional; if not set, statements will be printed instead if direct update) $setup->setConnectionName('{CONNECTION_NAME}'); // create database layout directly $setup->run(true); // display statements only $setup->run(false);

The place holders within the script have the following meaning:

  • {ENVIRONMENT}: This is the environment indicator of the application. It is used for addressing configuration files and must be adopted, if your environment is set to a value different than the default one. For details have a look at the Configuration chapter.
  • {CONTEXT}: This is the context of the application. It is used for addressing configuration files and must be adopted, if your environment is set to a value different than the default one. For details have a look at the Configuration chapter.
  • {CONFIG_NAMESPACE}: The namespace, that contains the mapper configuration files (see chapter 2.2).
  • {CONFIG_NAME_AFFIX}: The name affix for the configuration files (see chapter 2.1).
  • {CONNECTION_NAME}: The name of the database connection, that is used for the setup and for production use.

Moreover, please ensure that the database to be initialized exists before the setup script is started. Additionally, the user connecting to the database must have CREATE TABLE rights. Otherwise, the setup could not be done. If no error is displayed during setup, the setup has finished successfully. The result can be checked by using phpMyAdmin tool.

Executing the script to display the create statements only, the output should look like this:

SQL statement
CREATE TABLE IF NOT EXISTS `ent_application` ( `ApplicationID` INT(5) UNSIGNED NOT NULL auto_increment, `DisplayName` VARCHAR(100) character set utf8 NOT NULL default '', `CreationTimestamp` timestamp NOT NULL default CURRENT_TIMESTAMP, `ModificationTimestamp` timestamp NOT NULL default '0000-00-00 00:00:00', PRIMARY KEY (`ApplicationID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `ent_user` ( `UserID` INT(5) UNSIGNED NOT NULL auto_increment, `DisplayName` VARCHAR(100) character set utf8 NOT NULL default '', `FirstName` VARCHAR(100) character set utf8 NOT NULL default '', `LastName` VARCHAR(100) character set utf8 NOT NULL default '', `EMail` VARCHAR(100) character set utf8 NOT NULL default '', `Username` VARCHAR(100) character set utf8 NOT NULL default '', `Password` VARCHAR(100) character set utf8 NOT NULL default '', `CreationTimestamp` timestamp NOT NULL default CURRENT_TIMESTAMP, `ModificationTimestamp` timestamp NOT NULL default '0000-00-00 00:00:00', PRIMARY KEY (`UserID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ... CREATE TABLE IF NOT EXISTS `cmp_application2user` ( `CMPID` INT(5) UNSIGNED NOT NULL auto_increment, `Source_ApplicationID` INT(5) UNSIGNED NOT NULL default '0', `Target_UserID` INT(5) UNSIGNED NOT NULL default '0', PRIMARY KEY (`CMPID`), KEY `JOININDEX` (`ApplicationID`,`UserID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `cmp_application2role` ( `CMPID` INT(5) UNSIGNED NOT NULL auto_increment, `Source_ApplicationID` INT(5) UNSIGNED NOT NULL default '0', `Target_RoleID` INT(5) UNSIGNED NOT NULL default '0', PRIMARY KEY (`CMPID`), KEY `JOININDEX` (`ApplicationID`,`RoleID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Invoking setIndexColumnDataType() the data type of the columns that store the object and relation ids can be adapted. Concerning the sample statements above, the type was changed to INT(5) UNSIGNED. In case you need smaller or greater data rooms, you can adjust it using
PHP code
$setup->setIndexColumnDataType('TINYINT(3)');
Please be aware, that the value set with this method is directly used within the create statements. In case you are using invalid data types, this will lead to errors during setup of the tables.

Using phpMyAdmin you will get the following screen:

PHPMYAdmin view

Now, the configuration of the mapper has finished and you can use it.

3.2. Database update

The GenericORMapperManagementTool can also be used to apply changes on configuration files to existing database schemes.

In case no automated update is desired the GenericORMapperManagementTool provides a mode that displays the update statements rather to execute them directly. For huge databases, manual update should be the preferred way. This is especially true if index or row updates are included!

The update tool includes some limitations. For some MySQL versions, columns with DEFAULT values are updated by the GORM update tool even if it is not necessary. This will be fixed in one of the next releases if possible at all. But this fact does not limit the functionality of the tool it self!

The following script shows, how the automated database update can be done with aim of the GenericORMapperManagementTool tool. If you are searching for an example script, have a look at the /apps/modules/genericormapper/data/tools folder in the apf-codepack-* release. The file is named update.php.

In order to use the template, it must be adapted to fit your use case according to the notes below. The script is as follows:

PHP code
// include APF boostrap file require('./APF/core/bootstrap.php'); // configure the registry if desired use APF\core\registry\Registry; Registry::register('APF\core', 'Environment', '{ENVIRONMENT}'); use APF\modules\genericormapper\data\tools\GenericORMapperManagementTool; $update = new GenericORMapperManagementTool(); // set Context (important for the configuration files!) $update->setContext('{CONTEXT}'); // adapt storage engine (default is MyISAM) //$update->setStorageEngine('MyISAM|INNODB'); // adapt data type of the indexed columns, that are used for object ids //$update->setIndexColumnDataType('INT(5) UNSIGNED'); // initialize mapping configuration $update->addMappingConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // initialize relation configuration $update->addRelationConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // initialize database connection (optional; if not set, statements will be printed instead if direct update) $update->setConnectionName('{CONNECTION_NAME}'); // update database layout directly $update->run(true); // display statements only $update->run(false);

The place holders within the script have the following meaning:

  • {ENVIRONMENT}: This is the environment indicator of the application. It is used for addressing configuration files and must be adopted, if your environment is set to a value different than the default one. For details have a look at the Configuration chapter.
  • {CONTEXT}: This is the context of the application. It is used for addressing configuration files and must be adopted, if your environment is set to a value different than the default one. For details have a look at the Configuration chapter.
  • {CONFIG_NAMESPACE}: The namespace, that contains the mapper configuration files (see chapter 2.2).
  • {CONFIG_NAME_AFFIX}: The name affix for the configuration files (see chapter 2.1).
  • {CONNECTION_NAME}: The name of the database connection, that is used for the setup and for production use.

Further, it is important that the database that should be initialized must exist before setup is started. Additionally, the user connecting to the database must have CREATE TABLE, ALTER TABLE- and, if desired, ALTER INDEX-rights. Otherwise, the update could not be done. If no error is displayed during setup the setup has finished successfully. The result can be checked by using phpMyAdmin tool.

Please note some basic rules:
  • Huge databases should not be updated automatically!
  • Applying column changes, custom indices are not attended and can thus be deleted during update!

4. Usage of the o/r mapper

The o/r mapper, more precisely the GenericORRelationMapper, offers a list of API methods, that can be used for data and relation usage and manipulation. The following list gives you an overview of the existing functions and their meaning:

  • loadObjectListByCriterion(): Loads an object list by a criterion object.
  • loadObjectByCriterion(): Loads an object by a criterion object.
  • loadRelatedObjects(): Loads a list of objects, that are related to a dedicated object, by a relation key.
  • loadNotRelatedObjects(): Loads a list of objects by a relation key, that are *not* related to a dedicated object.
  • loadRelationMultiplicity(): Returns the number of objects, that are related to a dedicated object.
  • saveObject(): Saves an object or object tree, that consists of objects related to each other using the defined relations.
  • deleteObject(): Deletes an object. Associations and compositions are deleted as well.
  • createAssociation(): Creates an association between two objects.
  • deleteAssociation(): Deletes the associations between two objects.
  • deleteAssociations(): Deletes all associations that the given object has.
  • isAssociated(): Checks, if two objects are associated.
  • loadObjectList(): Loads a list of all objects.
  • loadObjectListByStatement(): Loads a list of objects by a statement.
  • loadObjectListByTextStatement(): Loads a list of objects by a statement present as a string.
  • loadObjectListByIDs(): Loads a list of objects by a list of ids.
  • loadObjectByStatement(): Loads an object by a statement.
  • loadObjectByTextStatement(): Loads an object by a statement present as a string.
  • loadObjectByID(): Loads an object by a given id.
  • loadObjectsWithRelation(): Loads a list of objects specified by the applied object type (object name as noted within the configuration) and the relation it should have.
  • loadObjectsWithoutRelation(): Loads a list of objects specified by the applied object type (object name as noted within the configuration) and the relation it should *not* have.
  • loadRelatedObject(): Loads a an object that is related to the current object using the applied relation name.

The *Statement* methods are provided due to performance optimization reasons. Details on the arguments can be taken from the API documentation for the modules.

4.1. Creating an instance of the mapper

4.1.1. Classic approach

The instance of the o/r mapper must be created using the GenericORMapperFactory. This is necessary, because the concrete mapper must be initialized prior to use and to have the possibility to use more than one instance per module. The latter one is probably not necessary within easy applications, but in complex environments this may be a knock-out criterion.

The next code box shows a typical call of the mapper:

PHP code
// create the factory using the desired service mode $ormFact = &$this->getServiceObject( 'APF\modules\genericormapper\data\GenericORMapperFactory'[, {SERVICE_OBJECT_TYPE}] ); // create the mapper using the factory $orm = &$ormFact->getGenericORMapper( {CONFIG_NAMESPACE}, {CONFIG_NAME_AFFIX}, {CONNECTION_NAME}[, $logStatements = false] );

The place holders have the following meaning:

  • {SERVICE_OBJECT_TYPE}: The type of service object of the factory. This param implicitly defines service type of the mapper. Allowed values NORMAL, SINGLETON, and SESSIONSINGLETON. Default is SINGLETON. Details can be taken from the Services.
  • {CONFIG_NAMESPACE}: The namespace, the configuration files reside (see chapter 2.2).
  • {CONFIG_NAME_AFFIX}: The name affix, the configuration files have (see chapter 2.1).
  • {CONNECTION_NAME}: The name of the database connection used for setup and usage.

Please note, that the factory service type also defines the service type of the mapper (=scope of the object concerning the service manager's instantiation model). In case, the GORM should be created only once per session due to performance reasons (mapping and relation tables are then only created once per visit) the factory creation statement must have the third param (service type) filled with the value SESSIONSINGLETON.

For development environments it is recommended to use the SINGLETON service type. Using the SESSIONSINGLETON type, the mapping and relation tables are only updated after the session has expired what may lead to unexpected results.

Further notes can be taken from the wiki page Typische Fehler beim GenericORMapper (German).

Please note, that the factory must be created by the getServiceObject() function to avoid unnecessary configuration side-effects.

In case, you want to activate statement logging for debugging reasons, the $logStatements must be set to true. Please do not use this option in production environments! Details can be taken from the API documentation.
4.1.2. Creation via DI

The GORM can also be created using the DIServiceManager. This approach implies advantages to testability and decoupling of business layer and presentation layer.

Creating an instance of the GORM is thus directly done by the DIServiceManager and not using the factory described above. This is because the DIServiceManager only accepts explicit service objects for dynamic initialization of other services. For this reason, three new services are available that do not contain any logic but the necessary configuration information:

  • GenericORMapperDIConfiguration: injection of the basic configuration
  • GenericORMapperDIMappingConfiguration: injection of additional mapping configurations
  • GenericORMapperDIRelationConfiguration: injection of additional relation configurations
Details on the configuration and an application sample can be found on the wiki page Erzeugen des GORM mit dem DIServiceManager (German).

The samples within the following chapters assume that there is a service configured for the instance of the o/r mapper named OR-Mapper and located under service namespace VENDOR\data\mapper:

APF configuration
[OR-Mapper] servicetype = "SINGLETON" class = "APF\modules\genericormapper\data\GenericORRelationMapper" setupmethod = "setup" conf.namespace.method = "setConfigNamespace" conf.namespace.value = "VENDOR\data\mapper" conf.debug.method = "setDebugMode" conf.debug.value = "true|false" init.db.method = "setDbDriver" init.db.namespace = "VENDOR\data\mapper" init.db.name = "DATABASE" init.rel.method = "addDIRelationConfiguration" init.rel.namespace = "VENDOR\data\mapper" init.rel.name = "CONFIG-RELATION" init.map.method = "addDIMappingConfiguration" init.map.namespace = "VENDOR\data\mapper" init.map.name = "CONFIG-MAPPING" [CONFIG-MAPPING] servicetype = "NORMAL" class = "APF\modules\genericormapper\data\GenericORMapperDIMappingConfiguration" conf.namespace.method = "setConfigNamespace" conf.namespace.value = "VENDOR\data\mapper" conf.affix.method = "setConfigAffix" conf.affix.value = "..." [CONFIG-RELATION] servicetype = "NORMAL" class = "APF\modules\genericormapper\data\GenericORMapperDIRelationConfiguration" conf.namespace.method = "setConfigNamespace" conf.namespace.value = "VENDOR\data\mapper" conf.affix.method = "setConfigAffix" conf.affix.value = "..." [DATABASE] servicetype = "SINGLETON" class = "APF\core\database\MySQLiHandler" setupmethod = "setup" conf.host.method = "setHost" conf.host.value = "..." conf.name.method = "setDatabaseName" conf.name.value = "..." conf.user.method = "setUser" conf.user.value = "..." conf.pass.method = "setPass" conf.pass.value = "..."

This configuration can be used to obtain an instance of the o/r mapper via

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper');

4.2. Loading data

To have a concrete example for the next code samples, the following UML diagram should be used. The picture contains the business object definition of the User management module module contained in the APF. The code presented here is thus taken from the module's code files.

Please note, that the o/r mapper can only manage objects that are known by the current instance. As described in chapter 6 the mapping and relation configuration can be enhanced by adding further configuration files.

Additional hints can be taken from The object name "Application" does not exist ... (German).

APF user management domain model

4.2.1. Loading objects

To load objects, you can use the functions

  • loadObjectByCriterion()
  • loadObjectByTextStatement()
  • loadObjectByStatement()
  • loadObjectByID()

If you intend to display the details of a user (see UML), you kan take the methods listed above to achieve this:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // load user (1) $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('UserID',1); $User = $ORM->loadObjectByCriterion('User',$Crit); // load user (2) $select = 'SELECT * FROM ent_user WHERE UserID = \'1\';'; $User = $ORM->loadObjectByTextStatement('User',$select); // load user (3) $User = $ORM->loadObjectByStatement('User', 'APF\modules\usermanagement', 'load_user_by_id'); // load user (4) $User = $ORM->loadObjectByID('User',1);

The content of the statement file load_user_by_id is:

SQL statement
SELECT * FROM ent_user WHERE UserID = '1';

Details on the statement execution can be taken from the statement files documentation.

GenericDomainObject brings some convenience methods to ease implementation. By typing
PHP code
$user = $orm->loadObjectByID('User', 1); echo $user->getObjectId();
you can easily retrieve the object's id without thinking about the id's property name. Using setObjectId() the object id can be set to the applied value. In cse you are interested in the object's name, just do the following:
PHP code
$user = $orm->loadObjectByID('User', 1); echo $user->getObjectName();
Further, the GenericDomainObject now supports PHP's automatic string conversion. Thus a user object can be printed like this for debug reasons:
PHP code
$user = $orm->loadObjectByID('User', 1); echo $user;
4.2.2. Loading lists
For loading lists, the o/r mapper features the following methods:
  • loadObjectList()
  • loadObjectListByCriterion()
  • loadObjectListByTextStatement()
  • loadObjectListByStatement()
  • loadObjectListByIDs()
If you like to display a list of users (see UML) you can use these methods as presented below:
PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // load user list (1) $UserList = $ORM->loadObjectList('User'); // load user list (2) $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('DisplayName','a%'); $UserList = $ORM->loadObjectListByCriterion('User',$Crit); // load user list (3) $select = 'SELECT * FROM ent_user WHERE DisplayName LIKE \'a%\';'; $UserList = $ORM->loadObjectListByTextStatement('User',$select); // load user list (4) $UserList = $ORM->loadObjectListByStatement('User', 'APF\modules\usermanagement', 'load_user_list'); // load user list (5) $UserList = $ORM->loadObjectListByIDs('User',array(1,2,3,4,5,6));
The content of the statement file load_user_list looks like this:
SQL statement
SELECT * FROM ent_user WHERE DisplayName LIKE 'a%';

If you have to display the corresponding groups while listing the user details, you can use the

  • loadRelatedObjects()

function. This method returns the objects related to a dedicated object by the desired relation key. The next example shows, how to load the groups associated to a user:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // load user list $Crit = new GenericCriterionObject(); $Crit->addOrderIndicator('DisplayName','ASC'); $UserList = $ORM->loadObjectListByCriterion('User',$Crit); // display the list including the associated groups for($i = 0; $ < count($UserList); $i++){ // display name of the user echo $UserList[$i]->getProperty('DisplayName'); // load corresponding groups $GroupList = $ORM->loadRelatedObjects($UserList[$i],'Group2User'); // display groups echo ' ,Gruppen: '; for($j = 0; $j < count($GroupList); $j++){ echo $GroupList[$j]->getProperty('DisplayName').' '; } }

To ease the handling of loading related objects, the GenericDomainObject posesses the loadRelatedObjects() function. This enables you to load the related objects using a relation key, where no instance of the mapper is available. To apply this to the example above, the user's groups could be loaded using

PHP code
$GroupList = $UserList[$i]->loadRelatedObjects('Group2User');

Note: The amount of objects loaded, can be limited using the GenericCriterionObject. So if you like to load the first 10 groups only, include the following code in your application:

PHP code
// define the limit $Crit = new GenericCriterionObject(); $Crit->addOrderIndicator('DisplayName','ASC'); $Crit->addPropertyIndicator('DisplayName','A%'); $Crit->addCountIndicator(10); // load the group list using the domain object $GroupList = $UserList[$i]->loadRelatedObjects('Group2User',$Crit); // load the group list using the mapper $GroupList = $ORM->loadRelatedObjects($UserList[$i],'Group2User',$Crit);

Often you have to select objects, that are not (yet) related with a given object, but a relation is defined. A good example, concerning the UML diagram above, is the listing of groups, that a certain user has not been added yet. For this purpose, the

  • loadNotRelatedObjects()

function could be used. The next example shows, how groups not associated with desired user can be selected:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // select user $Crit = new GenericCriterionObject(); $Crit->addpropertyIndicator('DisplayName','Mustermann, Max'); $User = $ORM->loadObjectByCriterion('User',$Crit); // select groups, that are not related with the user $GroupList = $ORM->loadNotRelatedObjects($User,'Group2User'); // present list for($i = 0; $ < count($GroupList); $i++){ echo '' . $GroupList[$i]->getProperty('DisplayName'); }

Note: The amount of objects loaded, can be limited using the GenericCriterionObject. A typical application case is to limit the list by relations to other objects. The following example explains, how to load all groups, that are not associated to the desired user but are composed under a Application object:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // select user $Crit = new GenericCriterionObject(); $Crit->addpropertyIndicator('DisplayName','Mustermann, Max'); $User = $ORM->loadObjectByCriterion('User',$Crit); // define additive relation criterion $Crit = new GenericCriterionObject(); $App = new GenericDomainObject('Application'); $App->setProperty('ApplicationID',1); $Crit->addRelationIndicator('Application2Group',$App); // select groups, that are not related with the user $GroupList = $ORM->loadNotRelatedObjects($User,'Group2User',$Crit); // present list for($i = 0; $ < count($GroupList); $i++){ echo '' . $GroupList[$i]->getProperty('DisplayName'); }

4.2.5. Loading the relation multiplicity

To be able to find out, how much objects are related to a certain one, the

  • loadRelationMultiplicity()

method can be used. It returns the multiplicity by a given object and a relation key. In order to count the users within a group, add the following code to your business layer:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // select group $Group = $ORM->loadObjectByID('Group',1); // select amount of users within the group echo $ORM->loadRelationMultiplicity($Group,'Group2User');

4.2.6 Loading the object count

Beneath the amount of objects related to another object, release 1.12 introduces a method to request the amount of objects of a certain kind stored in the database. For this reason, the

  • loadObjectCount()

method has been added. It takes the name of the object as defined in the mapping configuration as it's first parameter. As an optional second parameter, you can pass a GenericCriterionObject that limits the amount of objects using attribute indicators.

Requesting the amount of objects of type User and all users who's last name starts with character A can be requested as follows:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); $totalUsers = $ORM->loadObjectCount('User'); $crit = new GenericCriterionObject(); $crit->addPropertyIndicator('LastName','A%'); $usersWithA = $ORM->loadObjectCount('User',$crit);
Due to the fact, that this query is not cached, it should not be integrated in performance relevant parts of the application.

4.3. Saving objects

Saving objects is quite easy. Regardless you save objects or object trees, the

  • saveObject()

function can be used. The next code box shows how to save a user:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // set some attributes $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // save user $ORM->saveObject($user);
The saved object (in this case $user) can directly be reused after calling saveObject(). The mapper therefor injects the current mapper instance and the id of the object within the database. Details on the feature request can be taken from the forum post Erweiterung GORM (Release 1.11) (German).

4.4. Saving object trees

As already mentioned above, the o/r mapper is not only able to save flat object structures but also object trees. This feature can especially be used within your application's data layer to create the necessary relations.

Problem: When you create a User, it should be composed under the Application object. The latter one is used to ensure multi-client capability for the user management module.

Solution: To create the relation between these two objects (Application and User), the addRelatedObject() method of the GenericDomainObject can be used. The following code box shows, how the implementation is like:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // load application object $app = $ORM->loadObjectByID('Application',1); // fill user $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // compose user $app->addRelatedObject('Application2User',$user); // save object tree $ORM->saveObject($app);

Creating relations of type composition must be done exactly as described within the code block. This is because a composed object cannot live without it's father object. Associations can be added afterwards using createAssociation().

In case objects have to be created - as the user in the code snippet above - it is not necessary to save the user before creating it's composition with the application object. The GORM does this implicitly during saving the object tree (consisting of the Application, the User, and the relation between the two objects composing the user under the app).

If you intend to add a group and a role at the same time, the php code above must be added the following lines:

PHP code
// load application $app = $orm->loadObjectByID('Application',1); // fill user $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // load group $group = $orm->loadObjectByID('Group',1); // load role $role = $orm->loadObjectByID('Role',1); // associate role and group $user->addRelatedObject('Group2User',$group); $user->addRelatedObject('Role2User',$role); // compose user $app->addRelatedObject('Application2User',$user); // save object tree $orm->saveObject($app);

The generic O/R mapper is able to save object trees with various size. Please note, that it may be necessary to treat huge object trees a little bit different. But this is only necessary for applications with high performance requirements. In all other cases, the performance of the GORM is absolutely sufficient.

In case the amount of objects invoked during object tree persistence is greater than 20 including at least one relation per object, it is recommended to save the objects first by using saveObject(). Afterwards, the relations necessary can be created using createAssociation(). This kind of handling is only possible for associations and cannot be applied for compositions. This is because of the quality of composition relations.

4.5. Creation date of relations

It is possible to query the creation date of a relation between two objects. For this reason, the feature must be activated prior to use as described in chapter 2.5 for each relation.

Afterwards, the creation date can be queried as follows:

PHP code
$car = $gorm->loadObjectByID('Car', 1); $wheels = $car->getRelatedObjects('Car2Wheels'); foreach ($wheels AS $wheel) { echo 'Wheel mounted at: ' . $wheel->getRelationCreationTimestamp(); }

The return value of the getRelationCreationTimestamp() is a MySQL timestamp format. It may be reformatted using the PHP Date API.

In case no creation date is present you will receive null as it's value. This is when the feature is not activate for the current relation invokes or in case an existing data model has been updated and the current relation was created before the update.

5. The GenericCriterionObject

The present chapter is intended to describe the usage of the GenericCriterionObject. As already mentioned in the chapters above, the class can be used to configure your request rather than to write SQL statements. The object can be used with all load*ByCriterion() methods and while loading relation objects on existing domain objects or domain object lists.

5.1. Basics

The code box below presents an application use case of the GenericCriterionObject, where a list of users should be loaded. The users should be assigned to a special group and must be composed under a certain application object:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; class UsermanagementManager extends APFObject { public function getUserList() { /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // create the criterion object $Crit = new GenericCriterionObject(); // add the relation definition for the composition to the "Application" object $Application = new GenericDomainObject('Application'); $Application->setProperty('ApplicationID',1); $Crit->addRelationIndicator('Application2User',$Application); // add the relation definition for the association to the "Group" object $Group = new GenericDomainObject('Group'); $Group->setProperty('GroupID',1); $Crit->addRelationIndicator('Group2User',$Group); // add a limit indicator $Crit->addCountIndicator(0,3); // add a property indicator applied to the objects to be loaded $Crit->addPropertyIndicator('LastName','Achatz'); // add an order indicator $Crit->addOrderIndicator('FirstName','ASC'); $Crit->addOrderIndicator('LastName','DESC'); // add a directive, which properties should be loaded $Crit->addLoadedProperty('FirstName'); $Crit->addLoadedProperty('LastName'); // load an object list with aid of the criterion object or ... return $ORM->loadObjectListByCriterion('User',$Crit); // ... load an object with aid of the criterion object return $ORM->loadObjectByCriterion('User',$Crit); } }

Notes on the source code:

  • Relations: Adding a relation indicator to the criterion object says, that the object or object list to be loaded must have a relation to the object within the criterion. If a relation to the Application object (composition) and to the Group object (association) is added, this means that the resulting object list belongs to a dedicated application and is assigned to a special group. If a developer is likely to select all users, that belong to a dedicated application and are are member of a group and are assigned a role, three relation indicators must be added to the criterion object.
  • Sort sequence: The order of calls decides the sort sequences. If you like to sort in another direction, the sort indicators must be changed. ASC stands for ascending and DESC for descending sorting.
The GenericCriterionObject class offers a fluent interface. This can be used to cascade calls:
PHP code
$criterion = new GenericCriterionObject(); $criterion ->addCountIndicator(1) ->addRelationIndicator('xxx', $sourceObject) ->addOrderIndicator('name') ->addPropertyIndicator($attributeName, $attributeValue);
Further notes can be found on the Fluent Interface des GenericCriterionObject (German) wiki page.

5.2. Combination

The GenericCriterionObject provides the possibility to use enhanced logical operators such as AND OR, XOR, and NOT. To support this feature, the facility to combine criterion objects has been introduced. You are now enabled to create any mixture of operators you need.

The definition of the logical operator can be defined by setLogicalOperator() method invoked on any criterion. By default, AND is used to guarantee backward compatibility. To define the logical operator, just add these two lines of code:

PHP code
$criterion = new GenericCriterionObject(); $criterion->setLogicalOperator('OR');
The fluent interface is supported by this method, too.

Please note, that the logical operator is not applied to the whole criterion instance but is stored to be used for the next call of addPropertyIndicator().

PHP code
$criterion = new GenericCriterionObject(); $criterion->addPropertyIndicator('field1','value1') ->addPropertyIndicator('field2','value2') ->setLogicalOperator('OR') ->addPropertyIndicator('field3','value3') ->addPropertyIndicator('field4','value4');

The above code block creates the following condition. Please note, that the table names and the aliases the GORM uses are left out for better readability:

Code
[...] WHERE `field1`='value1' AND `field2`='value2' OR `field3`='value3' OR `field4`='value4'

As you can see with this example, the operator is stored until it's next change.

Due to the fact, that the last condition evaluates to true even if only `field4` contains 'value4' adding conditions directly after changing the the operator may often not make sense. In case you want to build up the condition to evaluate to true only if the `field1` must contain 'value1' and at least one of the other fields contains the defined value, the second block must be noted in brackets. You can realize this as follows:

PHP code
$criterion1 = new GenericCriterionObject(); $criterion2 = new GenericCriterionObject(); $criterion2->setLogicalOperator('OR') ->addPropertyIndicator('field2','value2') ->addPropertyIndicator('field3','value3') ->addPropertyIndicator('field4','value4'); $criterion1->addPropertyIndicator('field1','value1') ->addPropertyIndicator('field2+field3+field4',$criterion2);

As you can get from the addPropertyIndicator() call this method takes a string or a criterion object as it's second argument. This kind of combining criterion objects is not limited. Using the above code to execute a query, the subsequent sql is generated:

Code
[...] WHERE `field1`='value1' AND (`field2`='value2' OR `field3`='value3' OR `field4`='value4')
addPropertyIndicator() normally expects a string or numbers as it's second parameter. This is the reason why the second parameter cannot be passed as reference. In case of call-by-reference literal values or numbers had to be stored into variables before passing them to the method which is unnecessary from our point of view. Please note that this implies that the passed criterion objects are no references as well. For your daily business, this means that any changes to the applied criterion are not recognized after calling the addPropertyIndicator() method unless you are calling this method again using the modified criterion:
PHP code
$criterion1 = new GenericCriterionObject(); $criterion2 = new GenericCriterionObject(); $criterion2->setLogicalOperator('OR') ->addPropertyIndicator('field2','value2') ->addPropertyIndicator('field3','value3') ->addPropertyIndicator('field4','value4'); $criterion1->addPropertyIndicator('field1','value1') ->addPropertyIndicator('field2+field3+field4',$criterion2); // changes to the GCO for 'field4' $criterion2->addPropertyIndicator('field4','value4a'); $criterion1->addPropertyIndicator('field1','value1') // overwriting the value for 'field2+field3+field4' ->addPropertyIndicator('field2+field3+field4',$criterion2);

5.3. Comparison operators

The GenericCriterionObject supports configurable comparison operators. In the past, all comparisons have been done using the "=" sign. To be able to apply custom operators the addPropertyIndicator() has been added a third argument that takes the comparison operator.

PHP code
$criterion = new GenericCriterionObject(); $criterion->addPropertyIndicator('field1',15,'<');

The above sample shows the usage of the lesser than operator. As noted above, all types of operators can be used that are suitable for the database driver you use.

Please note, that the comparison operator is not validated. Hence, you are responsible to provide valid arguments. Otherwise, the database will answer your request with an exception!

6. Enhancement of mapping and relation table

If the GenericORRelationMapper should be used for several applications or multiple application cases, sometimes it is good to split the configuration into several pieces. This makes configuration more generic and the mapper can thus be enabled to use specific parts of a greater database. To do so, you have two different choices: you create one configuration set for each application case or you create only a basic configuration, that is interesting for all application cases (e.g. user management) and use the

  • addMappingConfiguration()
  • addRelationConfiguration()

to extend the configuration for the special use cas. With these functions, any object and relation configuration files can be added. The following example shows to you, how the functions can be used to upgrade the scope of the mapper:

PHP code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // add another object definition $ORM->addMappingConfiguration('APF\modules\usermanagement', 'umgt_2'); // add another relation definition $ORM->addRelationConfiguration('APF\modules\usermanagement', 'umgt_2');

The syntax of the object and relation configuration is the same as the default configuration described in the chapter 2.3. Object and relation definition. The additional object configuration file contains the following objects:

APF configuration
[Project] DisplayName = "VARCHAR(100)" Description = "TEXT" [News] DisplayName = "VARCHAR(100)" Title = "VARCHAR(100)" Content = "TEXT"
and the newly added relations are
APF configuration
[Application2Project] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "Project" [Project2News] Type = "COMPOSITION" SourceObject = "Project" TargetObject = "News"

7. Custom domain objects

In order to take a further step towards object orientation, the o/r mapper since version 1.14 supports the optional usage of custom domain objects. With an additional configuration file you can define, which object types (or rather database tables) should be represented by custom domain objects (instead of the GenericDomainObject) and where the object definitions are located.

This enables the possibility of adding own, object specific functions to the domain object, which required an extra manager object before. This leads to better readable code and makes the usage in many cases noticeable easier.

With this feature we also deliver a GenericORMapperDomainObjectGenerator, which is able to generate a ready definition for your custom domain objects, using your configuration files, which then can be edited as you need it. The generated definition consists of a base-object, which provides a getter- and setter-method for each property, defined in the database, and which must extend the GenericDomainObject or any object which already extends it. The object used for extending can be defined in the configuration, so that, in the easiest case, no more changes are needed after correct configuration and usage of the GenericORMapperDomainObjectGenerator. Furthermore, the generated definition contains the "real" domain object, which extends the base object, and which is designed to be changed from you. The base object must NOT be edited, because any change on it will be irrevocable removed when starting the generator (which also can be used as updater, because it always will generate the base objects new, which is important for any changes you make in the property configuration) again. For any changes you want to do, use the "real" domain object, because this will not be affected by the generator, since it already exists.

As another feature, the domain object can implement some event methods, which will be called from the o/r mapper when performing specific actions when working with this object, for example before and after saving it. This, for instance could be used to have arrays/objects as an property of the object, converting them to better storable JSON before saving, and reload the arrays/objects after saving. Doing so, the application never needs to take care of converting the data to the correct format, the object can do it on it's own. An overview of available event-methods can be found in the chapter Event methods.

The following chapters will show you how to configure, generate and use custom domain objects, and which events they support.

7.1. Configuration of custom domain objects

Whenever the o/r mapper should use custom domain objects, create an additional configuration file next to your mapper configurations:

  • {ENVIRONMENT}_{NAMEAFFIX}_domainobjects.ini

The place holders {ENVIRONMENT} and {NAMEAFFIX} can be adapted from the existing O/R mapper configurations.

In this file you need to define sections with the name of each object, which should be represented by a custom domain object, which is configured in your *_objects.ini. Objects, which do not need a custom domain object, don't need to be defined here, the GenericDomainObject will be automatically used for them.

Each section must define the following values:

  • Class: The fully-qualified class name of the object

If a base object should extend another object than the GenericDomainObject, and the generator will be used, also the following, otherwise not needed values need to be set:

  • Base.Class: The fully-qualified class name of the base-object which should be used

Important: Don't forget the fact, that your own base object must than extend the GenericDomainObject direct or indirect.

The following example is part of the configuration of the Postbox Extension (German), in which the custom domain objects were used first, and which can be used as secondary example for the usage of custom domain objects.

APF configuration
[Message] Class = "APF\extensions\postbox\biz\Message" Base.Class = "APF\extensions\postbox\bizAbstractMessage" [MessageChannel] Class = "APF\extensions\postbox\bizMessageChannel" Base.Class = "APF\extensions\postbox\bizAbstractMessageChannel"

Here, 2 objects got defined, which both should extend a special base-object.

7.2. Generating custom domain objects

Due to the delivered GenericORMapperDomainObjectGenerator the creating of custom domain objects is really easy. After creating the configuration files, you just need to create and start a little script, which generates the objects:

PHP code
// include APF bootstrap file require('./APF/core/bootstrap.php'); // configure environment if desired use APF\core\registry\Registry; Registry::register('APF\core', 'Environment', '{ENVIRONMENT}'); use APF\modules\genericormapper\data\tools\GenericORMapperDomainObjectGenerator; $generator = new GenericORMapperDomainObjectGenerator(); // set Context (important for the configuration files!) $generator->setContext('{CONTEXT}'); // initialize mapping configuration $generator->addMappingConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // initialize relation configuration $generator->addDomainObjectsConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); $generator->generateServiceObjects();

The place holder {CONTEXT} needs to be replaced with your context, {NAMESPACE} with the namespace where your configurations are stored (without context) and {NAMEAFFIX} with the affix you defined in the filename (see above).

The GenericORMapperDomainObjectGenerator will create the definition of each configured custom domain object, at the location defined in the configuration. If the file is already existing, the generator will try to regenerate the base object within this file, in order to let API-changes, made in your *_objects.ini, take effect. For that reason the file contains specific, corresponding named comments, which must not be edited or deleted, just as little as the code between them, otherwise the loss of data could be the consequence! When updating, the "real" domain object itself will not be changed, any changes you made on it should not get deleted. All changes on the base object will irrevocable be lost.

The APF team generally does not take liability for any problem/data loss which was caused from the generation, although we do test it, we never can exclude problems with it. You should always copy the existing files before using the generator, in order to be able to restore them if any problem occurs!

An example for the generated file (this one is taken from the message object defined above) could look like this:

PHP code
namespace APF\extensions\postbox\biz; //<*MessageBase:start*> DO NOT CHANGE THIS COMMENT! /** * Automatically generated BaseObject for Message. !!DO NOT CHANGE THIS BASE-CLASS!! * CHANGES WILL BE OVERWRITTEN WHEN UPDATING!! * You can change class "Message" which will extend this base-class. */ use APF\extensions\postbox\biz\AbstractMessage; class MessageBase extends AbstractMessage { public function __construct($objectName = null) { parent::__construct('Message'); } public function getText() { return $this->getProperty('Text'); } public function setText($value) { $this->setProperty('Text', $value); return $this; } public function getAuthorNameFallback() { return $this->getProperty('AuthorNameFallback'); } public function setAuthorNameFallback($value) { $this->setProperty('AuthorNameFallback', $value); return $this; } } // DO NOT CHANGE THIS COMMENT! <*MessageBase:end*> /** * Domain object for "Message" * Use this class to add your own functions. */ class Message extends MessageBase { /** * Call parent's function because the objectName needs to be set. */ public function __construct($objectName = null) { parent::__construct(); } }

The base object MessageBase extends AbstractMessage which was especially defined in the configuration. Even the import of the needed file was done from the generator. In *_objects.ini configuration of the o/r mapper, the message object has got the properties "Text" and "AuthorNameFallback", corresponding to this, getter- and setter-methods were created for them. The setters always get a fluent-interface.

Finally you see the definition of the Message object, which extends the base object. This is the class, you now are allowed to edit for your own needs, for example you could add a "delete()" method for deleting this message.

If you had a closer look at the Postbox-extension, you maybe will have seen, that there the delete-method was already defined in AbstractMessage. As you see this is also possible, but normally you won't need it.

7.3. Usage of custom domain objects

If the objects were created following the instructions above, we can come to the usage now. In fact, you don't need to take care of anything special here. Due to the fact that the objects always need to extend the GenericDomainObject they can be used like every other GenericDomainObject and are therefore downward compatible. This is particularly practical, because the decision to use the custom domain objects does not need any change on existing code.

The o/r mapper recognizes itself, due to the configuration, that it needs to create a custom domain object when loading data. That means, if you are loading any message (following the example above) from the database, you will now get a "Message"-object instead of a GenericDomainObject, which will have all your custom methods.

Whenever you want to generate a new object, you just import the corresponding class using an appropriate use statement and create an instance. Afterwards, working with the object is as before:

PHP code
use APF\extensions\postbox\biz\Message; $message = new Message(); $message->setText('ExampleText'); $orm->saveObject($message);

7.4. Event methods

As mentioned earlier, the o/r mapper offers some "events". The domain objects can define an event-method for each event, which will be called on specific actions. At the moment we offer the following event methods, which get called from the o/r mapper:

  • afterLoad(): Gets called after the object was created and filled with data from the database.
  • beforeSave(): Gets called before the object is being saved.
  • afterSave(): Gets called after the object has been saved to the database.

One possible example of using this events is the encoding end decoding of arrays or objects before/after saving or loading, which was mention in the Introduction.

7.5. Object trees

The generic o/r mapper offers a mechanism to create hierarchical object lists a.k.a. object trees. This feature is similar to nested set or parent id implementations.

In order to use this feature please define a domain object within {ENVIRONMENT}_{NAMEAFFIX}_domainobjects.ini that uses TreeItem as it's base class or directly uses TreeItem:

APF configuration
; Option 1 [NavigationNode] Class = "VENDOR\..\NavigationNode" Base.Class = "APF\modules\genericormapper\data\TreeItem" ; Option 2 [NavigationNode] Class = "APF\modules\genericormapper\data\TreeItem"

The hierarchy of objects can be done by defining a composition:

APF configuration
[NavigationNode2NavigationNode] Type = "COMPOSITION" SourceObject = "NavigationNode" TargetObject = "NavigationNode"

You can obtain the tree by using the loadObjectTree() method of the GenericORRelationMapper:

PHP code
$tree = $ORM->loadObjectTree('NavigationNode', 'NavigationNode2NavigationNode');

The method used within the above code box offers three further optional parameters:

  • criterion: To apply a GenericCriterionObject to specify the query with additional attributes.
  • rootObjectId: To specify the id if the object that is to be considered the root object of the tree.
  • maxDepth: Limits the depth of the tree to be loaded.

After loading the tree you can use two methods defined within TreeItem to traverse the structure. Using a recursive function you can display the object tree by a nested unordered list:

PHP code
function printChildObjects($objects) { echo '<ul>'; foreach ($objects as $object) { echo '<li>'; echo $object->getProperty('DisplayName'); $children = $object->getChildren(); if (count($children) > 0) { printChildObjects($children); } echo '</li>'; } echo '</ul>'; } $objectTree = $ORM->loadObjectTree('NavigationNode', 'NavigationNode2NavigationNode'); printChildObjects($objectTree);

8. Performance tricks

The o/r mapper is generally built for a maximum of performance but it may come to issues with sub-optimal implementation of higher software tiers or bad database or table setup.

This chapter lists some of the most common optimization patterns where manual interaction or manual adaption has positive impact on performance.

8.1. Frequent queries via attributes

In case queries make heavy use of attributes of objects to limit queries it is recommended to add an index to the respective row or rows.

Additional indices can be added along with the object definition that resides within the {ENVIRONMENT}_{NAMEAFFIX}_objects.ini files. Please note hints given in chapter 2.4. You may also manually add additional indices.

It is recommended to add necessary indices via object definition as these are automatically added/changed setting up the database (see chapter 3.1) or updating it (see chapter 3.2).

8.2. Creation of the mapper

Creating the internal mapping and relation table together with creating the database connection is one of the most expensive operations. Thus, it is recommended to create the GenericORRelationMapper in SESSIONSINGLETON or APPLICATIONSINGLETON mode (details see Creation of objects). This gains performance and saves you around 20% per request.

To create the mapper please use the second option of the APFObject::getServiceObject() method:

PHP code
$ormFact = &$this->getServiceObject( 'APF\modules\genericormapper\data\GenericORMapperFactory', APFService::SERVICE_TYPE_SESSION_SINGLETON ); $orm = &$ormFact->getGenericORMapper(..., ...);

Using the DIServiceManager to create the mapper pleaae set servicetype to the desired value. Example:

APF configuration
[OR-Mapper] servicetype = "SESSIONSINGLETON" class = "APF\modules\genericormapper\data\GenericORRelationMapper" ...

8.3. JOIN optimization

Executing a large number of JOIN operations per statement can cause statements to be slow in case indices are not defined or are not optimally defined. Facing a large number of records or having complicated queries it might be worth writing statements manually and execute them via load*ByStatement().

Please remember to place the most restrictive JOIN clause at the beginning to limit the amount of data processed ideally.

Please note, that the o/r mapper already defines JOIN indices for each relation. Example:
SQL statement
CREATE TABLE `cmp_application2role` ( `Source_ApplicationID` int(5) unsigned NOT NULL DEFAULT '0', `Target_RoleID` int(5) unsigned NOT NULL DEFAULT '0', KEY `JOIN` (`Source_ApplicationID` , `Target_RoleID`), KEY `REVERSEJOIN` (`Target_RoleID` , `Source_ApplicationID`) );
Hence, it is only recommended to add further indices to optimize statement execution time in critical and well-known cases!

9. Notes

The source files of the User management module module can be used as an extended example.

A further tutorial to get started with the mapper can be found in the Wiki (German).

Comments

Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
There are no comments belonging to this article.