This is an old revision of the document!
Table of Contents
Datarecord class
The Datarecord class is one of the core classes of Platform, as it provides a highly flexible object which can easily be stored in the database.
You can look for the Templateclass class which provides a blueprint for your own Datarecord class and can easily be altered to your own purpose. The Templateclass is meant as an example file, so you shouldn't subclass it, but just copy and rename it.
Quick start
There's three steps to get a new Datarecord class up and running.
1. Database table
protected static $database_table = 'DATABASE TABLE';
Alter the line above to select which table you want to store objects of this type into. (Don't create the table).
2. Object definition
Overwrite the buildStructure method to define which fields you want your object to consist of (see below).
3. Build table
Ensure to add the ensureInDatabase()
function of your new class to a suitable place in your code, which would most typically be the initializeDatabase()
function in your subclass of Instance.
Now your object is ready to use.
Object structure
You design the object by adding a structure to it, in the buildStructure()
function. The structure is defined by an array of arrays as defined below and is passed to the addStructure()
function.
Each element in the outer array represents a field and is hashed by the field name, while the inner array contains the field definition. The field definition contains the following fields:
label
The label is the human readable name of the field.
fieldtype
The fieldtype defines the possible content of the field and should refer to one of the following constants:
FIELDTYPE_KEY | The primary key which is an integer number and automatically assigned. Exactly one field must be of the FIELDTYPE_KEY type. |
---|---|
FIELDTYPE_ARRAY | An array of values. |
FIELDTYPE_BIGTEXT | A text of up to 16 mil. characters. |
FIELDTYPE_BOOLEAN | A boolean value. |
FIELDTYPE_CURRENCY | A currency value. |
FIELDTYPE_DATETIME | A datetime. |
FIELDTYPE_ENUMERATION | A enumeration. You need the enumeration property. |
FIELDTYPE_INTEGER | An integer number. |
FIELDTYPE_FILE | A reference to a file. You need the folder property. |
FIELDTYPE_FLOAT | A floating point number. |
FIELDTYPE_OBJECT | A complex object. |
FIELDTYPE_PASSWORD | A password, which can only be written or checked. |
FIELDTYPE_REFERENCE_SINGLE | A reference to another Datarecord object. You need to specify the class in the foreignclass property. |
FIELDTYPE_REFERENCE_MULTIPLE | A reference to several other Datarecord objects. A reference to another Datarecord object. You need to specify the class in the foreignclass property. |
FIELDTYPE_REFERENCE_HYPER | A reference to another Datarecord object, which can be of a different class from object to object. |
FIELDTYPE_TEXT | A text of up to 255 characters. |
Additional properties
Each field can have the following additional properties
enumeration | If the field is an enumeration, this should be the enumeration array, where the keys are the constants and the values are the visible values. |
---|---|
folder | If the field is a file field, this should specify which folder we store these files into. |
foreign_class | If the field is a reference to another class, this property should be the name of this class. |
form_size | Provides a hint to the form generator about the size of this field |
calculations | Determine what calculations should be performed on a remote object when this object is changed. See later. |
default_value | The default value of this field. |
invisible | A field of this type is invisible and cannot be viewed or edited by the user. |
is_title | If this is true, then this field is considered the title for the object. |
key | Indicates if this field should have a database key for improved performace. See below. |
required | The field is required, meaning that a form field representing this field will be mandatory. |
readonly | This field is readonly, which means it doesn't appear in forms. |
searchable | Indicates if this field is searchable. See below. Only work on text-based fields. |
store_in_database | Indicate if the field should be stored in the database. Defaults to true. See below. |
store_in_metadata | Each object contains a metadata field which is an array stored in a single database field (as json). If you specify this property, then the field will be saved into the metadata instead of having its own database field. This is useful for limiting the number of database fields, but makes the field unsuitable for searching or other high-performance operations. |
table | This field defines how the field is shown in tables. See constants below. |
tablegroup | If true tables will group by this field. |
table constants
COLUMN_VISIBLE | Show the column (but allow the user to hide it). This is the default setting if no value is passed. |
---|---|
COLUMN_HIDDEN | The column is hidden by default, but the user can select to show it. |
COLUMN_INVISIBLE | The column is hidden and cannot be selected to show. |
store_in_database
If this is set to false, the field will not be read or written to the database, but still be available. This can be used for calculated fields, where you then have to overwrite the getValue method in order to display data. Example: If you have two fields first_name and last_name, and also want a field full_name, you can implement full_name as a field not stored in the database.
protected static function buildStructure() { $structure = array( 'person_id' => array( 'invisible' => true, 'fieldtype' => self::FIELDTYPE_KEY ), 'first_name' => array( 'label' => 'First name', 'fieldtype' => self::FIELDTYPE_TEXT ), 'last_name' => array( 'label' => 'Last name', 'fieldtype' => self::FIELDTYPE_TEXT ), 'full_name' => array( 'label' => 'Full name', 'fieldtype' => self::FIELDTYPE_TEXT, 'store_in_database' => false ), ); self::addStructure($structure); parent::buildStructure(); } // We override this to make a special handling of the full_name field. // For all other fields we just call the parent function. public function getRawValue($field) { switch ($field) { case 'full_name': return $this->first_name.' '.$this->last_name; default: return parent::getRawValue($field); } }
Pro tip: Only use this for very light calculations with information already available on the object. It shouldn't be used for fields requiring heavy calculations or database lookup, as this can lead to poor performance.
Basic object usage
The object is created just like a regular object.
$user = new User();
To set a field use one of the following, which is equivalent:
$user->setValue('username', 'Michael Sahl'); $user->username = 'Michael Sahl';
To read a value use one of these:
$username = $user->getValue('username'); $username = $user->username;
There are more ways to read values from an object. This will be covered further down.
To save an object to the database:
$user->save();
This will automatically fill in the object key field (the field with type FIELDTYPE_KEY) with a new ID from the database, if it is the first time being saved. In addition it will only save the object is something have actually changed (which can be overwritten by passing true as the first parameter to save which will always save).
To load an object from the database, using an ID, use one of the following:
$user->loadForRead($id); $user->loadForWrite($id);
Loading an object for read, doesn't allow to save it. Loading it for write will block the object so another process cannot load it for write before this process is finished with it. This is to ensure that two processes doesn't change the same object at the same time. When saving an object it is switched back to read mode, unless explicitly held open by passing true as the second parameter:
$user->save(false, true);
If an object is opened for writing, but one decides not to save it, it can be unlocked like this:
$user->unlock();
…which also will switch it to read mode.
An object can be deleted from write mode by calling delete
, but one should check if we can delete it, by calling canDelete
first:
if ($user->canDelete()) $user->delete();
More about values
Values in the datarecord object can be accessed in four different modes.
Mode | Function | Explanation |
---|---|---|
RENDER_RAW | getRawValue() | This is the “raw” value, meaning the technical value of the field. If it is a reference, this is the ID to the foreign data. |
RENDER_FULL | getFullValue() | This is the display value, meaning the value to show to the end user. This is typically styled with html. |
RENDER_TEXT | getTextValue() | This is the display value, but suited for a text-only context, meaning that it isn't styled with html. |
RENDER_FORM | getFormValue() | This is the value as suited to pass into a proper form field. |
The basic functions to get and set values are setValue
and getValue
, and the datarecord also supports object notation to set and get values.
$user->setValue('name', 'Don Holmes'); // is equal to: $user->name = 'Don Holmes'; $name = $user->getValue('name'); // is equal to: $name = $user->name;
Pr. default getValue return raw values, but this behaviour can be modified by calling setDefaultRenderMode
which can select another type to return from getValue.
Object title
A datarecord object is considered to have a title, which is the human-readable way the object is presented when referred. The most simple way to assign a title is to just set the is_title
property to true on a field. For a more complex way, one can also overwrite the getTitle function.
Convenience functions
Getting a form
With a properly formed field definition, one can get a form for editing the object by just calling getForm()
. This will return a Form object ready to use.
The form will contain all editable fields in the same order that they are defined in the buildStructure
-function.
To place fields side-by-side, you can use the form_size property when defining the field. The property takes a percentage value, which dictates how much of the line the field should take up. As long as the line doesn't fill up, fields will be placed beside each other, so two fields with a size of 50% each, will be placed beside each other.
If one needs javascript to get the form to behave, then set the $edit_script
variable of the class to point to the javascript. In HTML the form will have an ID of [class_name]_form
Complete edit interface
Calling the renderEditComplex($parameters)
function will render a complete interface which will list all objects of this type, and allow the user to create, edit and delete these objects. The parameters are passed on to the Table listing the objects and can be used to extend functionality.
Also see DatarecordEditComplex class
References between Datarecords
It is easy to relate Datarecords to each other. To link a Datarecord to another datarecord add a field of field type FIELDTYPE_REFERENCE_SINGLE
or FIELDTYPE_REFERENCE_MULTIPLE
. The first will refer to a single object from the other datarecord class and the second will refer to multiple objects. When using these fields, you will also need to specify the foreignclass
-property which should be the full class name of the foreign datarecord class.
You will also need to go to the foreign datarecord class and add the class name of this datarecord class to either the $referring_classes
array or the $depending_classes
array. This ensures that the foreign class is aware of the connection. See the chapter about deleting data for an explanation between the two arrays and see the section Integrity check below, for an easy way to check if references are configured correctly.
Using references with non-Datarecord classes
It is possible to use references with non-datarecord classes, as long as these classes implements the DatarecordReferable
interface.
Deleting data and references
When deleting data, a system is in place for checking that no references are left to the deleted object. There are two strategies available, which can be used by setting the $delete_strategy
static variable in the datarecord, but before explaining these strategies, it's important to understand the two different class references we have.
In order for the delete system to function correctly, it is important that all classes that refers this class is mentioned in either the $referring_classes
array or the $depending_classes
array.
Classes in the $referring_classes
array should be classes, where the objects can refer this class but also can exist without such a reference. In a job database, we can have a class representing jobs and a class representing candidates. We can have a relation from a candidate to a job, to represent that the candidate is interested in the job. If the job then is deleted, we will still keep the candidate. Therefore we would put the candidate class in the $referring_classes
array of the job class, to indicate that if the job is deleted, we should just remove the reference from the candidate, but otherwise keep the candidate object.
In opposition, the $depending_classes
array should be classes, where the objects that refers this class cannot exist without such a reference. In a car database, we can have a class representing a car brand and another class representing the car model. We would have a relation from the model to the brand, but if the brand is deleted it wouldn't make sense to keep the models referring this brand, so therefore we would add the model class to the $depending_classes
array in the brand class, to indicate that if a brand is deleted, we should also delete all models referring to the brand.
And now to the strategies:
self::$delete_strategy = self::DELETE_STRATEGY_BLOCK;
This is the default strategy and this will prevent deletion of a datarecord if it is still references by other objects in the $referring_classes
array. This will make canDelete
return an error message if there are still objects referring the queried object, and will likewise prevent delete
from deleting anything as long as such references exists, except if the $force_purge
parameter is set to true, which will force the other delete strategy no matter what.
self::$delete_strategy = self::DELETE_STRATEGY_PURGE_REFERERS
Using this strategy, a call to delete
will go over all foreign objects of the classes mentioned in $referring_classes
, and remove all references to this object. Furthermore it will go over all foreign objects of the classes mentioned in $depending_classes
and outright delete these.
When using this strategy, a call to canDelete
will return true even though other objects references this object.
Copying objects
An object can be copied using the copy
function in the class, which will create an identical object in the database, with the only exception that the title field of the object (if any) will have “Copy of” prepended.
To make things more interesting one or more related classnames can be added as parameters to the function, and if this is done, all objects of these classes which relates to the main object is also copied and the new set of objects will relate to each other exactly like the source objects.
$family = new Family('Nielsen'); $dad = new FamilyMember('Anders'); $dad->family_reference = $family; $mom = new FamilyMember('Becca'); $mom->family_reference = $family; // This will copy the family object, along with the related data. $new_family = $family->copy(array('FamilyMember'));
Calculations
Sometimes a field in an object can be very expensive to update, either because it requires heavy computation or a lot of database queries. In this case one can request a calculation, which will basically start a background job which will perform the calculation so it doesn't disturb the rest of the script execution. This is accomplished by calling:
$object->requestCalculation('example_calculation');
Be aware that calculations will never be started before the script terminates, so one can never request a calculation to use in the current script.
Of course one would also have to implement the actual calculations which can be done in one of two ways:
Pr. object calculations
To do the calculation object by object, overwrite the doCalculation
function. The object will automatically be saved afterwards, so the function just needs to perform the calculation.
public function doCalculation($calculation) { switch ($calculation) { case 'example_calculation': // Do calculation and update necessary fields. // Don't save. This will be done afterwards. break; } }
Bulk calculations
If a calculation can benefit from being done on several objects at the same time, one can instead overwrite the doCalculationOnObjects
function, which is a static function and is just passed the calculation keyword and an array with all object ids which have requested this calculation. In this case one is responsible for both loading and saving the objects.
public static function doCalculationOnObjects($calculation, $ids) { // Do some common calculations for all these objects. foreach ($ids as $id) { $object = new exampleClass(); $object->loadForWrite($id); if ($object->isInDatabase()) { // Update object with information from calculation. $object->save(); } } }
Automatic calculation request on related class
If an object is related to another class with a field of type FIELDTYPE_REFERENCE_SINGLE
or FIELDTYPE_REFERENCE_MULTIPLE
and a change in the object always will require a calculation in objects of the related class, one can specify the calculation
property in the field description. This field should name one or more calculations in the foreign class which should be performed any time this object is updated.
'examplefield' => array( 'label' => 'Reference', 'fieldtype' => self::FIELDTYPE_REFERENCE_SINGLE, 'foreignclass' => 'Remoteclass', 'calculation' => array('calculation1', 'calculation2') )
This basically tells this object that every time it is updated, it should perform calculation1
and calculation2
on the object pointed to by the field examplefield
. This also works if examplefield
is changed, as it is checked both when the object is loaded and when it is saved.
If an object is saved but no changes are detected, then the calculation will not be performed.
Keys
Datarecords supports database keys for improved performance. To add a key to a field simply set the key-property to true.
To make a key spanning more than one field, instead set the key-property to the name(s) of the additional fields, separating field names by commas.
A call to ensureInDatabase()
will build the appropriate keys.
Integrity check
To ensure that a datarecord class is configured correctly, one can call the static function renderIntegrityCheck
on the class. This will render any errors which have been detected in the class setup, and is especially useful to check that references between classes are correctly configured.
This should only be used in a test environment and a good practice is to include a test-script which call this function on all datarecord classes.
Searching objects
Searching with keywords
It is possible to quickly find some objects containing certain keywords, by using the findByKeywords($keywords,$outputformat)
function. This will use filtering to search all objects for the keywords by looking in all fields marked as searchable. Each keyword is searched for individually, but phrases can also be searched by encapsulating them in quotes. A search of:
red “hairy cat”
will find all objects containing the word “red” and the phrase “hairy cat” in one or several of the searchable fields.
Output of the function defaults to a DatarecordCollection but can be altered to be a simple array or output suited for the FieldDatarecordcombobox field type.
Searching with SQL
It is possible to query the database directly, and a good way to do that is using the getCollectionFromSQL($sql)
function. Just be sure to always SELECT * from the proper table. This will add all the found records to a DatarecordCollection and return it.
$datacollection = User::getCollectionFromSQL("SELECT * FROM users WHERE age > 20 AND age < 45");
Access control
A number of methods exist to control what can be done with an object.
// Check if we are allowed to access / view this object. $object->canAccess(); // Check if we are allowed to create a new instance of this object. Class::canCreate(); // Check if we are allowed to edit this object. $object->canEdit();
These functions should return true if the operation is permitted, otherwise they should return false. They are all soft-checks meaning that there are no mechanisms build into the Datarecord class to enforce these permissions, so that should be done in the surrounding code. An exception to this is the renderEditComplex()
where the rendered interface will adapt to these functions.
If not overridden in a subclass then the functions just returns true.
// Check if we can delete an object. $object->canDelete();
This behaves a little different than the three other functions, as it is a hard-check meaning that Datarecord doesn't allow to delete an object which doesn't return true on this function. Furthermore the function doesn't return false, if deletion isn't allowed, but instead returns a string with a reason why one cannot delete this object.
Ensure consistency
A good practice is to implement the validateObject()
function in a Datarecord class. This function should test that the data inside the object is in a valid state. This function is typically called when something such as the api has manipulated the object data directly, to ensure that the object isn't left in an invalid state.
If you have an object with a $minimum_age
and $maximum_age
, you can validate that the former doesn't exceeds the latter like this:
public function validateObject() { if ($this->minimum_age > $this->maximum_age) return array('Minimum age must not be larger than maximum age'); return true; }