Сущности, файндеры и репозитории

Мы представили новую систему "Finder", которая позволяет создавать запросы программно объектно-ориентированным способом, так что не нужно писать необработанные запросы к базе данных. Система Finder работает рука об руку с системой Entity, о которой мы поговорим более подробно ниже. Первый аргумент, переданный в метод поиска, - это короткое имя класса для объекта, с которым вы хотите работать. Давайте просто преобразуем некоторые запросы, упомянутые в разделе выше, чтобы вместо этого использовать систему Finder. Например, чтобы получить доступ к одной записи пользователя:

Finder

Мы представили новую систему "Finder", которая позволяет создавать запросы программно объектно-ориентированным способом, так что не нужно писать необработанные запросы к базе данных. Система Finder работает рука об руку с системой Entity, о которой мы поговорим более подробно ниже. Первый аргумент, переданный в метод поиска, - это короткое имя класса для объекта, с которым вы хотите работать. Давайте просто преобразуем некоторые запросы, упомянутые в разделе выше, чтобы вместо этого использовать систему Finder. Например, чтобы получить доступ к одной записи пользователя:

$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->fetchOne();

Одно из основных различий между подходом прямого запроса и использованием Finder заключается в том, что базовая единица данных, возвращаемых Finder, не является массивом. В случае объекта Finder, который вызывает метод fetchOne (который возвращает только одну строку из базы данных), будет возвращен единственный объект Entity.

Давайте посмотрим на немного другой подход, который вернет несколько строк:

$finder = \XF::finder('XF:User');
$users = $finder->limit(10)->fetch();

Этот пример запросит 10 записей из таблицы xf_user и вернет их как объект ArrayCollection. Это специальный объект, который действует аналогично массиву, в том смысле, что он проходим (вы можете перебирать его в цикле), и у него есть некоторые специальные методы, которые могут сообщить вам общее количество записей, которые он имеет, группируя по определенным значениям или другому массиву, например, такие операции, как фильтрация, слияние, получение первой или последней записи и т. д.

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

Вместо этого, чтобы получить одно значение, вы должны просто выбрать одну сущность и прочитать значение непосредственно из нее:

$finder = \XF::finder('XF:User');
$username = $finder->where('user_id', 1)->fetchOne()->username;

Точно так же, чтобы получить массив значений из одного столбца, вы можете использовать метод pluckFrom:

$finder = \XF::finder('XF:User');
$usernames = $finder->limit(10)->pluckFrom('username')->fetch();

До сих пор мы видели, что Finder применяет довольно простые ограничения where и limit. Итак, давайте рассмотрим Finder более подробно, в том числе немного подробнее о самом методе where.

Метод where

Метод where может поддерживать до трех аргументов. Первое - это само условие, например столбец, который вы запрашиваете. Второй обычно был бы оператором. Третье - это значение, которое ищется. Если вы указываете только два аргумента, как вы видели выше, это автоматически подразумевает, что оператор =. Ниже приведен список других допустимых операторов:

  • =
  • <>
  • !=
  • >
  • >=
  • <
  • <=
  • LIKE
  • BETWEEN

Итак, мы можем получить список действующих пользователей, которые зарегистрировались за последние 7 дней:

$finder = \XF::finder('XF:User');
$users = $finder->where('user_state', 'valid')->where('register_date', '>=', time() - 86400 * 7)->fetch();

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

$finder = \XF::finder('XF:User');
$users = $finder->where([
    'user_state' => 'valid',
    ['register_date', '>=', time() - 86400 * 7]
])
->fetch();

Обычно не рекомендуется и не разрешается смешивать использование таким образом, но это в некоторой степени демонстрирует гибкость метода. Теперь, когда условия находятся в массиве, мы можем либо указать имя столбца (как ключ массива) и значение для подразумеваемого оператора =, либо мы можем фактически определить другой массив, содержащий столбец, оператор и значение.

Метод whereOr

В приведенных выше примерах должны быть соблюдены оба условия, т.е. каждое условие соединяется оператором AND. Однако иногда необходимо выполнить только часть Вашего условия, и это возможно с помощью метода whereOr. Например, если вы хотите найти пользователей, которые либо недействительны, либо не отправили ни одного сообщения, вы можете создать это следующим образом:

$finder = \XF::finder('XF:User');
$users = $finder->whereOr(
    ['user_state', '<>', 'valid'],
    ['message_count', 0]
)->fetch();

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

$finder = \XF::finder('XF:User');
$users = $finder->whereOr([
    ['user_state', '<>', 'valid'],
    ['message_count', 0],
    ['is_banned', 1]
])->fetch();

Метод with

Метод with по существу эквивалентен использованию синтаксиса INNER|LEFT JOIN, хотя он полагается на то, что для объекта Entity были определены его "Relations". Мы не будем вдаваться в это до следующей страницы, но это просто должно дать вам представление о том, как это работает. Теперь давайте воспользуемся средством поиска потоков для получения определенного потока:

$finder = \XF::finder('XF:Thread');
$thread = $finder->with('Forum', true)->where('thread_id', 123)->fetchOne();

Этот запрос будет извлекать сущность Thread, где thread_id = 123, но он также будет выполнять соединение с таблицей xf_forum за кулисами. С точки зрения управления выполнением INNER JOIN, а не LEFT JOIN, это то, для чего нужен второй аргумент. В этом случае мы установили для аргумента «должен существовать» значение true, поэтому он изменит синтаксис соединения на использование INNER, а не LEFT по умолчанию.

Мы более подробно рассмотрим, как получить доступ к данным, полученным из этого соединения, в следующем разделе.

Также можно передать массив отношений в метод with для выполнения нескольких соединений.

$finder = \XF::finder('XF:Thread');
$thread = $finder->with(['Forum', 'User'], true)->where('thread_id', 123)->fetchOne();

Это будет присоединено к таблице xf_user, чтобы также получить автора потока. Однако, поскольку второй аргумент все еще имеет значение true, нам может не потребоваться выполнять соединение INNER для присоединения пользователя, поэтому вместо этого мы могли бы просто связать методы:

$finder = \XF::finder('XF:Thread');
$thread = $finder->with('Forum', true)->with('User')->where('thread_id', 123)->fetchOne();

Методы order, limit и limitByPage

Метод order

Этот метод позволяет вам изменить ваш запрос так, чтобы результаты были получены в определенном порядке. Он принимает два аргумента: первый - это имя столбца, а второй - необязательно, направление сортировки. Итак, если вы хотите перечислить 10 пользователей, у которых больше всего сообщений, вы можете построить запрос следующим образом:

$finder = \XF::finder('XF:User');
$users = $finder->order('message_count', 'DESC')->limit(10);

Note

Теперь, вероятно, самое время упомянуть, что методы поиска обычно можно вызывать в любом порядке. Например: $threads = $finder->limit(10)->where('thread_id', '>', 123)->order('post_date')->with('User')->fetch(); Хотя если вы написали запрос MySQL в таком порядке, вы наверняка столкнетесь с некоторыми проблемами синтаксиса, система Finder все равно построит все в правильном порядке, и приведенный выше код, хотя и выглядит странно и, вероятно, не рекомендуется, но вполне допустим.

Как и в случае со стандартным запросом MySQL, можно заказать набор результатов по нескольким столбцам. Для этого достаточно снова вызвать метод заказа. Также можно передать несколько предложений order в метод order с помощью массива.

$finder = \XF::finder('XF:User');
$users = $finder->order('message_count', 'DESC')->order('register_date')->limit(10);

Метод limit

Мы уже видели, как ограничить запрос определенным количеством возвращаемых записей:

$finder = \XF::finder('XF:User');
$users = $finder->limit(10)->fetch();

Однако на самом деле есть альтернатива прямому вызову метода limit:

$finder = \XF::finder('XF:User');
$users = $finder->fetch(10);

вы можете передать свой лимит прямо в метод fetch(). Также стоит отметить, что метод limitfetch) поддерживает два аргумента. Первое, очевидно, является пределом, второе - смещением.

$finder = \XF::finder('XF:User');
$users = $finder->limit(10, 100)->fetch();

Значение смещения здесь по существу означает, что первые 100 результатов будут отброшены, а первые 10 после этого будут возвращены. Такой подход полезен для предоставления результатов с разбивкой на страницы, хотя на самом деле у нас также есть более простой способ сделать это...

Метод limitByPage

Этот метод является своего рода вспомогательным методом, который в конечном итоге устанавливает соответствующий предел и смещение в зависимости от «страницы», которую вы просматриваете в данный момент, и того, сколько «на страницу» вам требуется.

$finder = \XF::finder('XF:User');
$users = $finder->limitByPage(3, 20);

В этом случае предел будет установлен на 20 (что является нашим значением на страницу), а смещение будет установлено на 40, потому что мы начинаем со страницы 3.

Иногда нам необходимо получить больше данных, чем установлено. Чрезмерная выборка может быть полезна, чтобы помочь определить, есть ли у вас дополнительные данные для отображения после текущей страницы или вам нужно отфильтровать исходный набор результатов на основе разрешений. Мы можем сделать это с помощью третьего аргумента:

$finder = \XF::finder('XF:User');
$users = $finder->limitByPage(3, 20, 1);

Таким образом, начиная со страницы 3, вы получите до 21 пользователей (20 + 1).

Метод getQuery

Когда вы впервые начинаете работать с finder, каким бы интуитивным он ни был, вы можете иногда задаться вопросом, правильно ли вы его используете и собирается ли он строить запрос, который вы от него ожидаете. У нас есть метод с именем getQuery, который может сообщить нам текущий запрос, который будет построен с текущим объектом поиска. Например:

$finder = \XF::finder('XF:User')
    ->where('user_id', 1);

\XF::dumpSimple($finder->getQuery());

Это выведет что-то похожее на:

string(67) "SELECT `xf_user`.*
FROM `xf_user`
WHERE (`xf_user`.`user_id` = 1)"

Вероятно, он вам не понадобится очень часто, но он может быть полезен, если средство поиска не совсем возвращает ожидаемые результаты. Узнайте больше о методе dumpSimple в разделе Дамп переменной.

Пользовательские методы finder

До сих пор мы видели, как объект finder настраивается с аргументом, аналогичным XF:User и XF:Thread. По большей части это определяет класс Entity, с которым работает файндер, и разрешает, например, XF\Entity\User. Однако он может дополнительно представлять класс finder. Классы finder не являются обязательными, но они служат для добавления пользовательских методов finder к определенным типам средств finder. Чтобы увидеть это в действии, давайте посмотрим на класс поиска, связанный с XF:User, который можно найти в классе XF\Finder\User.

Вот пример метода finder из этого класса:

public function isRecentlyActive($days = 180)
{
    $this->where('last_activity', '>', time() - ($days * 86400));
    return $this;
}

Это позволяет нам теперь вызвать этот метод для любого объекта поиска пользователя. Итак, если мы возьмем пример ранее:

$finder = \XF::finder('XF:User');
$users = $finder->isRecentlyActive(20)->order('message_count', 'DESC')->limit(10);

Этот запрос, который ранее возвращал только 10 пользователей в убывающем порядке подсчета сообщений, теперь вернет 10 пользователей в этом порядке, которые были недавно активны в течение последних 20 дней.

Несмотря на то, что для многих типов сущностей класс поиска не существует, все же возможно расширить эти несуществующие классы таким же образом, как указано в разделе Расширение классов.

Система Entity

Если вы знакомы с XF1, возможно, вы знакомы с некоторыми концепциями, лежащими в основе Entities, поскольку они в конечном итоге заимствованы из системы DataWriter. Если вы не слишком знакомы с ними, следующий раздел должен дать вам некоторое представление.

Структура Entity

Объект Structure состоит из ряда свойств, которые определяют структуру Entity и таблицу базы данных, к которой он относится. Сам объект структуры настраивается внутри сущности, к которой он относится. Давайте посмотрим на некоторые общие свойства сущности User:

Таблица

$structure->table = 'xf_user';

Это сообщает Entity, какую таблицу базы данных использовать при обновлении и вставке записей, а также сообщает Finder, из какой таблицы следует читать при построении запросов для выполнения. Кроме того, он играет роль в знании того, к каким другим таблицам должен присоединиться ваш запрос.

Короткое имя

$structure->shortName = 'XF:User';

Это просто краткое имя класса как самой сущности, так и класса Finder (если применимо).

Тип контента

$structure->contentType = 'user';

Это определяет, какой тип контента представляет Entity. Это не понадобится в большинстве структур Entity. Он используется для подключения к определенным вещам, используемым системой «типов контента» (которые будут рассмотрены в другом разделе).

Первичный ключ

$structure->primaryKey = 'user_id';

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

Столбцы

$structure->columns = [
    'user_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true, 'changeLog' => false],
    'username' => ['type' => self::STR, 'maxLength' => 50,
        'required' => 'please_enter_valid_name'
    ]
    // и многие другие столбцы...
];

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

На основе type диспетчер сущностей знает, нужно ли кодировать или декодировать значение определенным образом. Это может быть несколько простой процесс преобразования значения в строку или целое число или несколько более сложный, например использование json_encode() в массиве при записи в базу данных или использование json_decode() в строке JSON, когда чтение из базы данных, чтобы значение было правильно возвращено объекту сущности в виде массива без необходимости делать это вручную. Он также может поддерживать соответствующее кодирование/декодирование значений, разделенных запятыми.

Иногда необходимо выполнить дополнительную проверку или модификацию значения, прежде чем оно будет записано. В качестве примера в сущности User рассмотрим метод verifyStyleId(). Когда значение устанавливается в поле style_id, мы автоматически проверяем, существует ли метод с именем verifyStyleId(), и если это так, мы сначала пропускаем значение через него.

Поведение

$structure->behaviors = [
    'XF:ChangeLoggable' => []
];

Это массив классов поведения, которые должны использоваться этой сущностью. Классы поведения - это способ, позволяющий повторно использовать определенный код в целом для нескольких типов сущностей (только при изменении сущности, а не при чтении). Хорошим примером этого является поведение XF:Likeable, которое способно автоматически выполнять определенные действия с объектами, которые поддерживают контент, который может быть "лайкнут". Это включает в себя автоматический пересчет количества, когда в контенте происходят изменения видимости, и автоматическое удаление лайков при удалении контента.

Геттеры

$structure->getters = [
    'is_super_admin' => true,
    'last_activity' => true
];

Методы получения автоматически вызываются при вызове именованных полей. Например, если мы запрашиваем is_super_admin у объекта User, он автоматически проверяет и использует метод getIsSuperAdmin(). Интересно отметить, что в таблице xf_user на самом деле нет поля с именем is_super_admin. На самом деле он существует в сущности Admin, но мы добавили его как метод получения в качестве сокращенного способа доступа к этому значению. Методы получения также могут использоваться для непосредственного переопределения значений существующих полей, что имеет место для значения last_activity здесь. last_activity на самом деле является кешированным значением, которое обычно обновляется, когда пользователь выходит из системы. Однако мы храним дату последней активности пользователя в таблице xf_session_activity, поэтому мы можем использовать этот метод getLastActivity для возврата этого значения вместо кэшированного значения последнего действия. Если вам когда-либо понадобится полностью обойти метод получения и просто получить истинное значение сущности, просто добавьте к имени столбца знак подчеркивания, например, $user->last_activity_.

Поскольку объект такой же, как и любой другой объект PHP, вы можете добавить к нему больше методов. Обычно это используется для добавления таких вещей, как методы проверки разрешений, которые могут быть вызваны для самой сущности.

Отношения

$structure->relations = [
    'Admin' => [
        'entity' => 'XF:Admin',
        'type' => self::TO_ONE,
        'conditions' => 'user_id',
        'primary' => true
    ]
];

Так определяются отношения. Какие отношения? Они определяют отношения между сущностями, которые могут использоваться для выполнения запросов на соединение с другими таблицами или для извлечения записей, связанных с сущностью, на лету. Если мы помним метод with в finder, если бы мы хотели получить определенного пользователя и предварительно получить запись администратора пользователя (если она существует), то мы бы сделали что-то вроде следующего:

$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('Admin')->fetchOne();

Это будет использовать информацию, определенную в пользовательской сущности для отношения Admin и детали структуры сущности XF:Admin, чтобы знать, что этот пользовательский запрос должен выполнить LEFT JOIN для таблицы xf_admin и столбца user_id. Чтобы получить доступ к дате последнего входа в систему администратора от пользователя:

$lastLogin = $user->Admin->last_login; // возвращает отметку времени последнего входа в систему администратора

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

$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->fetchOne();
$lastLogin = $user->Admin->last_login; // возвращает отметку времени последнего входа в систему администратора

Здесь мы по-прежнему получаем значение last_login. Он делает это, выполняя дополнительный запрос, чтобы на лету получить сущность Admin.

В приведенном выше примере используется тип TO_ONE, и поэтому это отношение связывает одну сущность с другой. У нас также есть тип TO_MANY.

Невозможно получить полное отношение TO_MANY (например, с помощью метода соединения / with в finder), но за счет запроса можно прочитать это в любое время на лету, например, в последний пример last_login выше.

Одно из таких отношений, которое определено для объекта User, - это отношение ConnectedAccounts:

$structure->relations = [
    'ConnectedAccounts' => [
        'entity' => 'XF:UserConnectedAccount',
        'type' => self::TO_MANY,
        'conditions' => 'user_id',
        'key' => 'provider'
    ]
];

Это отношение может возвращать записи из таблицы xf_user_connected_account, которые соответствуют текущему идентификатору пользователя как FinderCollection. Это похоже на объект ArrayCollection, который мы упоминали в разделе Finder выше. Определение отношения указывает, что коллекция должна быть привязана к полю provider.

Хотя невозможно получить несколько записей при выполнении запроса файндера, можно использовать отношение TO_MANY для извлечения единственной записи из этого отношения. Например, если мы хотим узнать, связан ли пользователь с определенным поставщиком подключенной учетной записи, мы можем, по крайней мере, получить его при запросе:

$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('ConnectedAccounts|facebook')->fetchOne();

Опции

$structure->options = [
    'custom_title_disallowed' => preg_split('/\r?\n/', $options->disallowedCustomTitles),
    'admin_edit' => false,
    'skip_email_confirm' => false
];

Параметры объекта - это способ изменить поведение объекта при определенных условиях. Например, если мы установим для параметра admin_edit значение true (что имеет место при редактировании пользователя в Admin CP), тогда определенные проверки будут пропущены, например, чтобы адрес электронной почты пользователя был пустым.

Жизненный цикл Entity

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

  • _preSave() - Это происходит до начала процесса сохранения и в основном используется для выполнения любых дополнительных проверок перед сохранением или для установки дополнительных данных перед сохранением.
  • _postSave() - После того, как данные были сохранены, но до того, как какие-либо транзакции будут зафиксированы, этот метод вызывается, и вы можете использовать его для выполнения любой дополнительной работы, которая должна сработать после сохранения объекта.

Кроме того, существуют _preDelete() и _postDelete(), которые работают аналогичным образом, но когда происходит удаление.

Entity также может предоставить информацию о своем текущем состоянии. Например, есть методы isInsert() и isUpdate(), чтобы вы могли определить, вставляется ли это новая запись или обновляется существующая запись. Существует метод isChanged(), который может сказать Вам, изменилось ли конкретное поле с момента последнего сохранения.

Давайте посмотрим на некоторые реальные примеры этих методов в действии в сущности User.

 protected function _preSave()
 {
    if ($this->isChanged('user_group_id') || $this->isChanged('secondary_group_ids'))
    {
        $groupRepo = $this->getUserGroupRepo();
        $this->display_style_group_id = $groupRepo->getDisplayGroupIdForUser($this);
    }

    // ...
 }

 protected function _postSave()
 {
    // ...

    if ($this->isUpdate() && $this->isChanged('username') && $this->getExistingValue('username') != null)
    {
        $this->app()->jobManager()->enqueue('XF:UserRenameCleanUp', [
            'originalUserId' => $this->user_id,
            'originalUserName' => $this->getExistingValue('username'),
            'newUserName' => $this->username
        ]);
    }

    // ...

В примере _preSave() мы извлекаем и кэшируем новый идентификатор группы отображения для пользователя на основе их измененных групп пользователей. В примере _postSave() мы запускаем задание после изменения имени пользователя.

Repositories

Репозитории - это новая концепция для XF2, но вас не могут обвинить в сравнении их с объектами «Модель» из XF1. У нас нет объекта модели в XF2, потому что у нас есть гораздо лучшие места и способы для выборки и записи данных в базу данных. Итак, вместо того, чтобы иметь массивный класс, который содержит все запросы, необходимые Вашему дополнению, и все различные способы управления этими запросами, у нас есть файндер, который добавляет гораздо больше гибкости.

Также стоит иметь в виду, что в XF1 объекты модели были своего рода «свалкой» для многих вещей. Многие из них сейчас избыточны. Например, в XF1 весь код восстановления разрешений находился в модели разрешений. В XF2 у нас есть определенные службы и объекты, которые этим занимаются.

Итак, что такое репозитории? Они соответствуют сущности и методам поиска и удержания, которые обычно возвращают настройку объекта поиска для определенной цели. Почему бы просто не вернуть результат запроса файндера? Что ж, если мы вернем сам объект поиска, тогда он послужит полезной точкой расширения для надстроек, чтобы расширить его и изменить объект поиска до того, как объект или коллекция будет возвращена.

Репозитории также могут содержать некоторые специальные методы для таких вещей, как восстановление кеша.