There are three methods available for writing inside the described class: BookTable::add, BookTable::update, BookTable:delete.
BookTable::add
The add-entry method admits as an input parameter an array with values containing entity field names as the keys:
namespace SomePartner\MyBooksCatalog;
use Bitrix\Main\Type;
$result = BookTable::add(array(
'ISBN' => '978-0321127426',
'TITLE' => 'Patterns of Enterprise Application Architecture',
'PUBLISH_DATE' => new Type\Date('2002-11-16', 'Y-m-d')
));
if ($result->isSuccess())
{
$id = $result->getId();
}
The method returns the result object Entity\AddResult, and the example above shows how to check the successful adding of an entry and obtain the ID of the added entry.
Note. The objects of the class Bitrix\Main\Type\Date and Bitrix\Main\Type\DateTime must be used as values of the fields DateField and DateTimeField and also for user-defined fields Date and Date with Time. By default, the designer receives a line date in the website format but the format of the date to be submitted can also be indicated explicitly.
Attention! The fields must be used in upper case: FIELDS. This field in lower case is reserved for needs of the system. In similar fashion, the field auth_context is reserved by the system as well.
BookTable::update
Entry update follows a similar procedure; only the value of the primary key is added to the array of values in the parameters:
$result = BookTable::update($id, array(
'PUBLISH_DATE' => new Type\Date('2002-11-15', 'Y-m-d')
));
In the example, the date indicated in the new entry is corrected. As a result, the object Entity\UpdateResult is returned, and it also has a test method isSuccess() (to make sure that there was no errors in the query) and, additionally, it is possible to learn whether the entry was actually updated: getAffectedRowsCount().
BookTable::delete
Only the primary key is needed to delete the record:
$result = BookTable::delete($id);
Operation Results
If one or more errors occur during the operation, their text can be obtained from the result:
$result = BookTable::update(...);
if (!$result->isSuccess())
{
$errors = $result->getErrorMessages();
}
Default Values
Sometimes the majority of new entries always contain the same value for a certain field or it is calculated automatically. Let us assume that the book catalog has today’s date as the issue/publishing date by default (it is logical to add a book to the catalog on the day of its issue). Let us return to the description of the field in the entity and use the parameter default_value:
new Entity\DateField('PUBLISH_DATE', array(
'default_value' => new Type\Date
))
Now, when adding an entry with no expressly indicated issue date, its value will be the current day:
$result = BookTable::add(array(
'ISBN' => '978-0321127426',
'TITLE' => 'Some new book'
));
Let us consider a more complicated task: there is no possibility to promptly add books on the day of their issue but it is known that, as a rule, new books are issued on Fridays. Accordingly, they will be added only in the course of the following week:
new Entity\DateField('PUBLISH_DATE', array(
'default_value' => function () {
// figure out last friday date
$lastFriday = date('Y-m-d', strtotime('last friday'));
return new Type\Date($lastFriday, 'Y-m-d');
}
))
Any callable value can be the value of the parameter default_value: a function name, an array from class/object and a name of the method, or an anonymous function.
Validators
Before writing new data to the database their correctness must be checked without fail. This can be done with the help of validators:
new Entity\StringField('ISBN', array(
'required' => true,
'column_name' => 'ISBNCODE',
'validation' => function() {
return array(
new Entity\Validator\RegExp('/[\d-]{13,}/')
);
}
))
Now each time you add or edit an entry, the ISBN will be checked using the template [\d-]{13,}
– the code must contain only numbers and a hyphen, with a minimum of 13 digits.
Validation is set using the parameter validation in the field designer and is a callback that returns an array of validators.
Note: Why validation – callback, and not just an array of validators? It is a kind of deferred load: validators will be instantiated only when data validation is really needed. Generally no validation is needed for data sampling from the database.
An inheritor Entity\Validator\Base
or any callable that is to return a true or a text of an error, or an object of Entity\FieldError
(if you want to use own code of the error) is accepted as a validator.
It is known for sure that the ISBN code must contain 13 digits, and these digits can be separated by several hyphens:
978-0321127426
978-1-449-31428-6
9780201485677
To make sure that there are exactly 13 digits, let us write our own validator:
new Entity\StringField('ISBN', array(
'required' => true,
'column_name' => 'ISBNCODE',
'validation' => function() {
return array(
function ($value) {
$clean = str_replace('-', '', $value);
if (preg_match('/^\d{13}$/', $clean))
{
return true;
}
else
{
return ‘The ISBN code must contain 13 digits.’;
}
}
);
}
))
The value of this field is submitted to the validator as the first parameter, but more optional information is also available:
new Entity\StringField('ISBN', array(
'required' => true,
'column_name' => 'ISBNCODE',
'validation' => function() {
return array(
function ($value, $primary, $row, $field) {
// value – field value
// primary – an array with the primary key, in this case [ID => 1]
// row – all arrays of data submitted to ::add or ::update
// field – an object of the field under validation – Entity\StringField('ISBN', ...)
}
);
}
))
This set of data allows for a much wider range of complex checks.
If several validators are attached to a field and there is a need to find out on the program level which of them exactly has worked, the error code can be used. For example, the last digit of the ISBN code is a control digit that serves to check the correctness of the numerical part of ISBN. You have to add a validator for checking it and to process its result in a specific way:
// describing validator in the entity field
new Entity\StringField('ISBN', array(
'required' => true,
'column_name' => 'ISBNCODE',
'validation' => function() {
return array(
function ($value) {
$clean = str_replace('-', '', $value);
if (preg_match('/^\d{13}$/', $clean))
{
return true;
}
else
{
return ‘ISBN code must contain 13 digits.’;
}
},
function ($value, $primary, $row, $field) {
// checking the last digit
// ...
// if the number is wrong, a special error is returned
return new Entity\FieldError(
// if the number is wrong, a special error is returned
);
}
);
}
))
// performing the operation
$result = BookTable::update(...);
if (!$result->isSuccess())
{
// checking which errors have been revealed
$errors = $result->getErrors();
foreach ($errors as $error)
{
if ($error->getCode() == 'MY_ISBN_CHECKSUM')
{
// our validator has worked
}
}
}
2 standard error codes are available by default: BX_INVALID_VALUE if the validator has worked, and BX_EMPTY_REQUIRED if no required field is indicated when adding an entry.
Validators work both when adding new entries and when updating the existing entries. This behavior is based on the general purpose of validators consisting in guaranteeing correct and integral data in the database. The event mechanism is available in order to check data only upon their addition or updating and also for other manipulations.
We recommend that you use standard validators in standard situations:
Entity\Validator\RegExp
– check by regular expression,
Entity\Validator\Length
– check the minimum/maximum line length,
Entity\Validator\Range
– check the minimum/maximum number value,
Entity\Validator\Unique
– check the uniqueness of a value.
The validators described above cannot apply to the User-defined fields. Their values shall be configured in field settings through the administrative interface.
Events
In the example with validators, one of the checks for the ISBN field consisted in checking the availability of 13 digits. In addition to numbers, ISBN code may include hyphens, but technically speaking they have no value. In order to store only “clean” data in the database (13 digits only, without hyphens), we can use an internal event handler:
class BookTable extends Entity\DataManager
{
...
public static function onBeforeAdd(Entity\Event $event)
{
$result = new Entity\EventResult;
$data = $event->getParameter("fields");
if (isset($data['ISBN']))
{
$cleanIsbn = str_replace('-', '', $data['ISBN']);
$result->modifyFields(array('ISBN' => $cleanIsbn));
}
return $result;
}
}
The method onBeforeAdd set up in the entity is automatically recognized by the system as a handler for the event “before addition” thus allowing change of data or additional checks to be done in it. In the example, we have changed the ISBN code using the method modifyFields.
// before transformation
978-0321127426
978-1-449-31428-6
9780201485677
// after transformation
9780321127426
9781449314286
9780201485677
After such a transformation, we can return again to the neat validator RegExp instead of using an anonymous function (because we already know that the value will contain no acceptable hyphens and only numbers must remain):
'validation' => function() {
return array(
//function ($value) {
// $clean = str_replace('-', '', $value);
//
// if (preg_match('/^\d{13}$/', $clean))
// {
// return true;
// }
// else
// {
// return 'The ISBN code must contain 13 digits.';
// }
//},
new Entity\Validator\RegExp('/\d{13}/'),
...
);
}
In addition to data change, the event handler makes it possible to delete data or even abort the operation. For example, let us assume that the updating of the ISBN code for the books that already exist in the catalog must be prohibited. It can be done in the event handler onBeforeUpdate using one of two ways:
public static function onBeforeUpdate(Entity\Event $event)
{
$result = new Entity\EventResult;
$data = $event->getParameter("fields");
if (isset($data['ISBN']))
{
$result->unsetFields(array('ISBN'));
}
return $result;
}
In this option, the ISBN will be deleted “with no fuss” as if it were not submitted. The second option consists in prohibiting its update and generating an error:
public static function onBeforeUpdate(Entity\Event $event)
{
$result = new Entity\EventResult;
$data = $event->getParameter("fields");
if (isset($data['ISBN']))
{
$result->addError(new Entity\FieldError(
$event->getEntity()->getField('ISBN'),
'Changing the ISBN code for the existing books is prohibited'
));
}
return $result;
}
If an error is returned, we have formed the object Entity\FieldError
in order for us to learn during subsequent error processing in which field, exactly, the check was activated. If the error applies to more than one field or to the entire entry, the use of the object Entity\EntityError
will be more appropriate:
public static function onBeforeUpdate(Entity\Event $event)
{
$result = new Entity\EventResult;
$data = $event->getParameter("fields");
if (...) // comprehensive data check
{
$result->addError(new Entity\EntityError(
'Impossible to update an entry'
));
}
return $result;
}
Two events were used in the examples: onBeforeAdd and onBeforeUpdate, there are nine such events in total:
- onBeforeAdd (parameters: fields)
- onAdd (parameters: fields)
- onAfterAdd (parameters: fields, primary)
- onBeforeUpdate (parameters: primary, fields)
- onUpdate (parameters: primary, fields)
- onAfterUpdate (parameters: primary, fields)
-
onBeforeDelete (parameters: primary)
- onDelete (parameters: primary)
- onAfterDelete (parameters: primary)
The following diagram shows the sequence in which the event handlers are called, and the actions a handler may carry out.
It goes without saying that these events can be handled in the entity itself as well as in the methods with the same name. In order to subscribe to an event in an arbitrary point of script execution, call for the event manager:
$em = \Bitrix\Main\ORM\EventManager::getInstance();
$em->addEventHandler(
BookTable::class, // entity class
DataManager::EVENT_ON_BEFORE_ADD, // event code
function () { // your callback
var_dump('handle entity event');
}
);
Value formatting
Sometimes it may become necessary to store data in one format and work with them in the program in another. The most common example: work with an array and its serialization before saving into the database. For this, the field parameters save_data_modification and fetch_data_modification are available. They are set up similarly to the validators through callback.
Let us use the example of a book catalog in order to describe the text field EDITIONS_ISBN: it will store the ISBN codes of other editions of the book, if any:
new Entity\TextField('EDITIONS_ISBN', array(
'save_data_modification' => function () {
return array(
function ($value) {
return serialize($value);
}
);
},
'fetch_data_modification' => function () {
return array(
function ($value) {
return unserialize($value);
}
);
}
))
We have indicated the serialization of the value before saving into the database in the parameter save_data_modification, and we have set up de-serialization during sampling from the database in the parameter fetch_data_modification. Now, when writing business logic you can simply work with the array without having to look into conversion issues.
Attention! Before creating a serialized field, make sure the serialization will not interfere during filtering or linking tables. Search by a single value in WHERE among serialized lines is highly inefficient. You may want to opt for
a normalized data storage scheme.
Since serialization is the most typical example for conversion of values it is singled out into a separate parameter serialized:
new Entity\TextField('EDITIONS_ISBN', array(
'serialized' => true
))
However, you can still describe your callables for other data modification options.
Value calculating
More often than not, developers have to implement counters where a new value is to be calculated on the database side for the sake of data integrity instead of selecting the old value and recalculating it on the application side. In other words, the queries of the following type must be executed:
UPDATE my_book SET READERS_COUNT = READERS_COUNT + 1 WHERE ID = 1
If the numeric field, READERS_COUNT is described in the entity, the counter increment can be launched as follows:
BookTable::update($id, array(
'READERS_COUNT' => new DB\SqlExpression('?# + 1', 'READERS_COUNT')
));
The placeholder ?# means that the following argument in the designer is the database ID – the name of the database, table, or column, and this value will be masked appropriately. For all variable parameters, the use of placeholders is highly recommended. This approach will help to avoid problems with SQL injections.
For example, if an increment number of readers is variable, it would be better to describe the expression as follows:
// correct
BookTable::update($id, array(
'READERS_COUNT' => new DB\SqlExpression('?# + ?i', 'READERS_COUNT', $readersCount)
));
// incorrect
BookTable::update($id, array(
'READERS_COUNT' => new DB\SqlExpression('?# + '.$readersCount, 'READERS_COUNT')
));
The list of placeholders currently available:
- ? or ?s – the value is masked in put between single quotes '
- ?# – the value is masked as an identifier
- ?i – the value is reduced to integer
- ?f – the value is reduced to float
Error warnings
The examples above have a peculiarity that the data update query is called without checking the result.
// call without checking successful query execution
BookTable::update(...);
// with check
$result = BookTable::update(...);
if (!$result->isSuccess())
{
// error processing
}
The second option is undoubtedly preferable from the point of view of control. However, if the code is executed only in the agent mode, we have no use for the list of errors occurred during validation. In this case, if the query has not gone through due to a “failed” validation and isSuccess() check was not called, the system will generate E_USER_WARNING with a list of errors which may be seen in the website log (provided that .settings.php is set up properly).
In view of the results of this chapter, some changes have occurred in the entity description. It looks as follows now:
namespace SomePartner\MyBooksCatalog;
use Bitrix\Main\Entity;
use Bitrix\Main\Type;
class BookTable extends Entity\DataManager
{
public static function getTableName()
{
return 'my_book';
}
public static function getUfId()
{
return 'MY_BOOK';
}
public static function getMap()
{
return array(
new Entity\IntegerField('ID', array(
'primary' => true,
'autocomplete' => true
)),
new Entity\StringField('ISBN', array(
'required' => true,
'column_name' => 'ISBNCODE',
'validation' => function() {
return array(
new Entity\Validator\RegExp('/\d{13}/'),
function ($value, $primary, $row, $field) {
// check the last digit
// ...
// if the digit is incorrect we will return a special error
return new Entity\FieldError(
$field, 'ISBN control digit does not match', 'MY_ISBN_CHECKSUM'
);
}
);
}
)),
new Entity\StringField('TITLE'),
new Entity\DateField('PUBLISH_DATE', array(
'default_value' => function () {
// figure out last friday date
$lastFriday = date('Y-m-d', strtotime('last friday'));
return new Type\Date($lastFriday, 'Y-m-d');
}
)),
new Entity\TextField('EDITIONS_ISBN', array(
'serialized' => true
)),
new Entity\IntegerField('READERS_COUNT')
);
}
public static function onBeforeAdd(Entity\Event $event)
{
$result = new Entity\EventResult;
$data = $event->getParameter("fields");
if (isset($data['ISBN']))
{
$cleanIsbn = str_replace('-', '', $data['ISBN']);
$result->modifyFields(array('ISBN' => $cleanIsbn));
}
return $result;
}
}
Copy this code and play with all the options described above.