Перейти к содержанию

Операции с сущностями

Тему урока можно изучить в новом формате — в документации по Bitrix Framework. В ней улучшена структура, описание, примеры.

Для операций записи используются три метода уже описанного нами класса: BookTable::add, BookTable::update, BookTable::delete.

BookTable::add

Метод для добавления записи принимает на вход массив со значениями, где ключи - имена полей сущности:

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();
}

Метод возвращает объект результата Entity\AddResult, и в примере выше показано, как проверить успешность добавления и получить ID добавленной записи.

Примечание. Для значений полей типов DateField и DateTimeField, а также для пользовательских полей Дата и Дата со временем, необходимо использовать объекты классов Bitrix\Main\Type\Date и Bitrix\Main\Type\DateTime. По умолчанию в конструктор передается строковая дата в формате сайта, но можно и явно указать формат передаваемой даты.

Внимание! Поле fields необходимо использовать в верхнем регистре: FIELDS. В нижнем регистре это поле зарезервировано для нужд системы. Аналогично зарезервировано и поле auth_context.

BookTable::update

Обновление записи происходит похожим образом, только к массиву значений в параметрах добавляется значение первичного ключа:

$result = BookTable::update($id, array(
    'PUBLISH_DATE' => new Type\Date('2002-11-15', 'Y-m-d')
));

В примере исправлена неправильно указанная при добавлении дата. В качестве результата возвращается объект Entity\UpdateResult, у которого так же есть проверочный метод isSuccess() (не было ли ошибок в запросе), и, дополнительно, можно узнать была ли запись фактически обновлена: getAffectedRowsCount().

BookTable::delete

Для удаления записи нужен только первичный ключ:

$result = BookTable::delete($id);

Примечание. Для удаления или обновления составного ключа следует передавать оба значения:

BookTable::delete(['key1' => value1, 'key2' => value2]);

Результаты операции

Если во время операции произошла одна или несколько ошибок, их текст можно получить из результата:

$result = BookTable::update(...);

if (!$result->isSuccess())
{
    $errors = $result->getErrorMessages();
}

Значения по умолчанию

Бывает, что у большинства новых записей значение какого-то поля всегда одно и то же, или вычисляется автоматически. Пусть у каталога книг дата издания/публикации по умолчанию будет сегодняшним днем (логично добавлять книгу в каталог сразу в день ее выхода). Вернемся к описанию поля в сущности и используем параметр `default_value`:

new Entity\DateField('PUBLISH_DATE', array(
    'default_value' => new Type\Date
))

Теперь при добавлении записи без явного указания даты издания ее значением будет текущий день:

$result = BookTable::add(array(
    'ISBN' => '978-0321127426',
    'TITLE' => 'Some new book'
));

Усложнение задачи: если нет возможности оперативно добавлять книги в день их выхода, но известно, что, как правило, новые книги выходят по пятницам. Соответственно, они добавлены будут только на следующей неделе:

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');
    }
))

Значением параметра `default_value` может быть любой `callable`: имя функции, массив из класса/объекта и названия метода, или анонимная функция.

Валидаторы

Перед записью новых данных в БД нужно обязательно проверять их на корректность. Для этого предусмотрены валидаторы:

new Entity\StringField('ISBN', array(
    'required' => true,
    'column_name' => 'ISBNCODE',
    'validation' => function() {
        return array(
            new Entity\Validator\RegExp('/[\d-]{13,}/')
        );
    }
))

Теперь при добавлении и изменении записей ISBN будет проверен по шаблону [\d-]{13,} - код должен содержать только цифры и дефис, минимум 13 цифр.

Валидация задается параметром 'validation' в конструкторе поля и представляет собой callback, который возвращает массив валидаторов.

Примечание: Почему validation - callback, а не сразу массив валидаторов? Это своего рода отложенная загрузка: валидаторы будут инициализированы только тогда, когда действительно нужна будет валидация данных. В большинстве же случаев - при выборке данных из БД - валидация не нужна.

В качестве валидатора принимается наследник Entity\Validator\Base или любой callable, который должен вернуть true, или текст ошибки, или объект Entity\FieldError (в случае, если вы хотите использовать собственный код ошибки).

Точно известно, что в ISBN коде должно быть 13 цифр, эти цифры могут разделять несколько дефисов:

978-0321127426
978-1-449-31428-6
9780201485677

Чтобы удостовериться, что цифр там именно 13, напишем свой собственный валидатор:

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 должен содержать 13 цифр.';
                }
            }
        );
    }
))

Первым параметром в валидатор передается значение данного поля, но опционально доступно больше информации:

new Entity\StringField('ISBN', array(
    'required' => true,
    'column_name' => 'ISBNCODE',
    'validation' => function() {
        return array(
            function ($value, $primary, $row, $field) {
                // value - значение поля
                // primary - массив с первичным ключом, в данном случае [ID => 1]
                // row - весь массив данных, переданный в ::add или ::update
                // field - объект валидируемого поля - Entity\StringField('ISBN', ...)
            }
        );
    }
))

С таким набором данных можно произвести гораздо больший спектр сложных проверок.

Если к полю приписано несколько валидаторов, и есть необходимость программно узнать, какой конкретно из них сработал, можно воспользоваться кодом ошибки. Например, у кода ISBN последняя цифра - контрольная, служит для проверки правильности числовой части ISBN. Надо добавить валидатор для ее проверки и обработаем его результат особым образом:

// описываем валидатор в поле сущности
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 должен содержать 13 цифр.';
                }
            },
            function ($value, $primary, $row, $field) {
                // проверяем последнюю цифру
                // ...
                // если цифра неправильная - возвращаем особую ошибку
                return new Entity\FieldError(
                    $field, 'Контрольная цифра ISBN не сошлась', 'MY_ISBN_CHECKSUM'
                );
            }
        );
    }
))
// выполняем операцию
$result = BookTable::update(...);

if (!$result->isSuccess())
{
    // смотрим, какие ошибки были выявлены
    $errors = $result->getErrors();

    foreach ($errors as $error)
    {
        if ($error->getCode() == 'MY_ISBN_CHECKSUM')
        {
            // сработал наш валидатор
        }
    }
}

По умолчанию есть 2 стандартных кода ошибки: BX_INVALID_VALUE, если сработал валидатор, и BX_EMPTY_REQUIRED, если при добавлении записи не указано обязательное required поле.

Валидаторы срабатывают как при добавлении новых записей, так и при обновлении существующих. Такое поведение исходит из общего назначения валидаторов - гарантировать корректные и целостные данные в БД. Для проверки данных только при добавлении или только при обновлении, а также для других манипуляций существует механизм событий.

В типовых случаях рекомендуем вам использовать штатные валидаторы:

  • Entity\Validator\RegExp - проверка по регулярному выражению,
  • Entity\Validator\Length - проверка на минимальную/максимальную длину строки,
  • Entity\Validator\Range - проверка на минимальное/максимальное значение числа,
  • Entity\Validator\Unique - проверка на уникальность значения

Описанные валидаторы не применимы к Пользовательским полям — проверка их значений конфигурируется в настройках поля через административный интерфейс.

События

В примере с валидаторами одной из проверок поля ISBN была проверка на наличие 13 цифр. Помимо цифр, в ISBN коде могут встречаться дефисы, но с технической точки зрения они не несут никакой ценности. Чтобы хранить в БД "чистые" данные - только 13 цифр, без дефисов - можно воспользоваться внутренним обработчиком события:

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

Метод onBeforeAdd, определенный в сущности, автоматически распознается системой как обработчик события "перед добавлением", и в нем можно изменить данные или провести дополнительные проверки. В приведенном примере мы изменили поле ISBN посредством метода modifyFields.

// до преобразования
978-0321127426
978-1-449-31428-6
9780201485677

// после преобразования
9780321127426
9781449314286
9780201485677

После такого преобразования можно вновь вернуться к лаконичному валидатору RegExp вместо анонимной функции (ведь мы уже знаем, что допустимых дефисов в значении не будет, должны остаться только цифры):

'validation' => function() {
    return array(
        //function ($value) {
        //  $clean = str_replace('-', '', $value);
        //
        //  if (preg_match('/^\d{13}$/', $clean))
        //  {
        //      return true;
        //  }
        //  else
        //  {
        //      return 'Код ISBN должен содержать 13 цифр.';
        //  }
        //},
        new Entity\Validator\RegExp('/\d{13}/'),
        ...
    );
}

Помимо изменения данных, в обработчике события можно удалить данные или вовсе прервать выполнение операции. Например, необходимо запретить обновление ISBN кода для уже существующих в каталоге книг. Сделать это можно в событии onBeforeUpdate двумя способами:

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

В таком варианте ISBN будет "тихо" удален из набора данных, будто его и не передавали. Второй способ запретить его обновлять - сгенерировать ошибку:

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'),
            'Запрещено менять ISBN код у существующих книг'
        ));
    }

    return $result;
}

В случае возврата ошибки мы сформировали объект Entity\FieldError для того, чтобы впоследствии при обработке ошибок знать, на каком именно поле сработала проверка. Если ошибка относится к нескольким полям или целиком ко всей записи, то более уместно будет воспользоваться объектом Entity\EntityError:

public static function onBeforeUpdate(Entity\Event $event)
{
    $result = new Entity\EventResult;
    $data = $event->getParameter("fields");

    if (...) // комплексная проверка данных
    {
        $result->addError(new Entity\EntityError(
            'Невозможно обновить запись'
        ));
    }

    return $result;
}

В примерах использовались два события: onBeforeAdd и onBeforeUpdate, всего же таких событий девять:

  • OnBeforeAdd (параметры: fields)
  • OnAdd (параметры: fields)
  • OnAfterAdd (параметры: fields, primary)

  • OnBeforeUpdate (параметры: primary, fields)

  • OnUpdate (параметры: primary, fields)
  • OnAfterUpdate (параметры: primary, fields)

  • OnBeforeDelete (параметры: primary)

  • OnDelete (параметры: primary)
  • OnAfterDelete (параметры: primary)

Порядок вызова событий и допустимые действия в обработчиках каждого из них:

Конечно же, обрабатывать эти события можно не только в самой сущности в одноименных методах. Чтобы подписаться на событие в произвольном месте выполнения скрипта, нужно вызвать менеджер событий:

$em = \Bitrix\Main\ORM\EventManager::getInstance();

$em->addEventHandler(
    BookTable::class, // класс сущности
        DataManager::EVENT_ON_BEFORE_ADD, // код события
        function () { // ваш callback
            var_dump('handle entity event');
        }
);

Форматирование значений

Иногда может возникнуть необходимость хранить данные в одном формате, а работать с ними в программе уже в другом. Самый распространенный пример: работа с массивом и его сериализация перед сохранением в БД. На этот случай предусмотрены параметры поля 'save_data_modification' и 'fetch_data_modification'. Определяются они аналогично валидаторам, через callback.

На примере каталога книг опишем текстовое поле EDITIONS_ISBN: оно будет хранить коды ISBN других изданий книги, если таковые имеются:

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);
            }
        );
    }
))

В параметре save_data_modification мы указали сериализацию значения перед сохранением в БД, а в параметре fetch_data_modification рассериализацию при выборке из БД. Теперь при написании бизнес-логики вы можете просто работать с массивом, не отвлекаясь на вопросы конвертации.

Внимание! Прежде чем создать у себя сериализованное поле, подумайте не помешает ли сериализация при фильтрации или связывании таблиц. Искать по одиночному значению в WHERE среди сериализованных строк крайне неэффективно. Возможно, вам больше подойдет нормализованная схема хранения данных.

Поскольку сериализация - это наиболее типичный пример для конвертации значений, она вынесена в отдельный параметр serialized:

new Entity\TextField('EDITIONS_ISBN', array(
    'serialized' => true
))

Но вы по-прежнему можете описать свои callable для других вариантов модификации данных.

Вычисляемые значения

Очень часто разработчики сталкиваются с реализацией счетчиков, где для целостности данных предпочтительно рассчитывать новое значение на стороне БД, вместо выборки старого значения и пересчете его на стороне приложения. Другими словами, нужно выполнять запросы вида:

UPDATE my_book SET READERS_COUNT = READERS_COUNT + 1 WHERE ID = 1

Если описать числовое поле READERS_COUNT в сущности, то инкремент счетчика можно будет запустить следующим образом:

BookTable::update($id, array(
    'READERS_COUNT' => new DB\SqlExpression('?# + 1', 'READERS_COUNT')
));

Плейсхолдер ?# означает, что следующим аргументом в конструкторе идет идентификатор БД - имя базы данных, таблицы или колонки, и это значение будет экранировано соответствующим образом. Для всех изменяемых параметров рекомендуется обязательно использовать плейсхолдеры - такой подход поможет избежать проблем с SQL инъекциями.

Например, если инкрементируемое число читателей переменно, то лучше описать выражение так:

// правильно
BookTable::update($id, array(
    'READERS_COUNT' => new DB\SqlExpression('?# + ?i', 'READERS_COUNT', $readersCount)
));

// неправильно
BookTable::update($id, array(
    'READERS_COUNT' => new DB\SqlExpression('?# + '.$readersCount, 'READERS_COUNT')
));

Список доступных на данный момент плейсхолдеров:

  • ? или ?s - значение экранируется и заключается в кавычки '
  • ?# - значение экранируется как идентификатор
  • ?i - значение приводится к integer
  • ?f - значение приводится к float

Предупреждения об ошибках

В предыдущих примерах есть нюанс: запрос на обновление данных вызывается без проверки результата:

// вызов без проверки успешности выполнения запроса
BookTable::update(...);

// с проверкой
$result = BookTable::update(...);
if (!$result->isSuccess())
{
    // обработка ошибки
}

Несомненно, второй вариант более предпочтителен с точки зрения контроля происходящего. Но если код выполняется только в режиме агента, нам некому и незачем показывать список возникших в процессе валидации ошибок. В таком случае, если запрос не прошел из-за "проваленной" валидации, и не была вызвана проверка isSuccess(), система сгенерирует E_USER_WARNING со списком ошибок, который можно будет увидеть в логе сайта (если соответствующим образом настроить .settings.php).

По результатам данной главы произошли некоторые изменения в описании сущности, теперь оно выглядит так:

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) {
                            // проверяем последнюю цифру
                            // ...
                            // если цифра неправильная - возвращаем особую ошибку
                            return new Entity\FieldError(
                                $field, 'Контрольная цифра ISBN не сошлась', '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;
    }
}

Скопировав этот код, вы можете поэкспериментировать со всеми описанными выше возможностями.