CakePHP 4 — как проверять формы, которым необходимо сохранять данные в несколько таблиц

Извините, если об этом уже спрашивали. Все примеры, которые я могу найти, устарели или относятся к устаревшим версиям CakePHP, например. cakephp: сохранение нескольких моделей с использованием одной формы составляет 7 лет. Старый.

У меня есть приложение в CakePHP 4.1.6. Две таблицы в базе данных называются tbl_users и tbl_orgs (организации в данном случае означают организации).

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

Проблема, с которой я сталкиваюсь, заключается в том, как заставить форму работать таким образом, чтобы она запускала правила проверки для оба tbl_users и tbl_orgs при отправке.

Вот как сейчас устроено наше приложение:

Существует метод Controller с именем add() в src/Controller/TblOrgsController.php. Это было сгенерировано bake и изначально использовалось для вставки новой организации в таблицу tbl_orgs. На данный момент он ничего не сделал с точки зрения tbl_users, однако сработал с точки зрения сохранения новой организации и запуска соответствующих правил проверки.

Одно правило проверки состоит в том, что каждая запись companyname в tbl_orgs должна быть уникальной. Если вы попытаетесь вставить более 1 компании с названием My Company Limited, это приведет к ошибке проверки Это название компании уже существует:

// src/Model/Table/TblOrgsTable.php
public function buildRules(RulesChecker $rules): RulesChecker
{
    $rules->add(
        $rules->isUnique(['companyname']),
        [
            'errorField' => 'companyname',
            'message' => 'This company name already exists',
        ]
    );

    return $rules;
}

В то время как вышеприведенное относится к TblOrgs, у нас также есть buildRules() в TblUsers, который применяет аналогичную логику к полю email, чтобы убедиться, что все адреса электронной почты уникальны для каждого пользователя.

В методе add() Controller мы начинаем с указания новой пустой сущности для TblOrgs:

// src/Controller/TblOrgsController.php
public function add()
{
    $org = $this->TblOrgs->newEmptyEntity();
    // ...
    $this->set(compact('org'));
}

Когда форма создана, мы передаем $org:

// templates/TblOrgs/add.php
<?= $this->Form->create($org) ?>
<?= $this->Form->control('companyname') ?>
<?= $this->Form->end() ?>

Когда TblOrgs поля отображаются браузером, мы можем проверить HTML и увидеть, что они подчиняются соответствующей Модели. Это ясно из-за таких вещей, как required="required" и maxlength="100", которые соответствуют тому факту, что поле не может быть пустым и является полем VARCHAR(100) в базе данных:

<input type="text" name="companyname" required="required" id="companyname" maxlength="100"> 

Это также работает с точки зрения правил, указанных в buildRules для TblOrgs. Например, если я дважды ввожу одно и то же название компании, отображается соответствующая ошибка:

введите здесь описание изображения

Затем я попытался ввести поля для TblUsers. Я префикс полей формы с точечной нотацией, например. это должно соответствовать полю ввода tbl_users.email:

<?= $this->Form->control('TblUser.email') ?>

При проверке HTML он не делает того же, что и для TblOrgs. Например, такие вещи, как maxlength или required, отсутствуют. Он фактически не знает о TblUsers. Я понимаю, что $org в моем методе Controller указывает новую сущность для TblOrgs, а не TblUsers. Я просмотрел документацию CakePHP по Сохранение с ассоциациями что говорит

Метод save() также может создавать новые записи для ассоциаций.

Однако в документации приведен пример:

$firstComment = $articlesTable->Comments->newEmptyEntity();
// ...
$tag2 = $articlesTable->Tags->newEmptyEntity();

В этом случае Tags отличается от Comments Моделью, но newEmtpyEntity() работает для обоих. Имея это в виду, я адаптировал свой метод add() к следующему:

$org = $this->TblOrgs->TblUsers->newEmptyEntity();

Но теперь это дает Entity для TblUsers. Кажется, у вас может быть и то, и другое, но не то и другое.

Причина, по которой это не работает для моего варианта использования, заключается в том, что я могу либо запустить свои правила проверки для TblOrgs (но не TblUsers), либо наоборот.

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

Для справки имеется соответствующая связь между двумя таблицами:

// src/Model/Table/TblOrgsTable.php
public function initialize(array $config): void
{
    $this->hasMany('TblUsers', [
        'foreignKey' => 'o_id',
        'joinType' => 'INNER',
    ]);
}

а также

// src/Model/Table/TblUsersTable.php
public function initialize(array $config): void
{
    $this->belongsTo('TblOrgs', [
        'foreignKey' => 'o_id',
        'joinType' => 'INNER',
    ]);
}

person Andy    schedule 23.02.2021    source источник
comment
Во-первых, я не думаю, что ваши отношения на самом деле устроены правильно. Каковы на самом деле отношения между ними? Это не может быть hasMany в обоих направлениях, это отношение belongsToMany. Или же один hasMany, а другой belongsTo. Только после того, как с этим разобрались, мы можем точно сказать, как вы должны называть свои поля. Однако я могу подтвердить, что TblUser.field неверен.   -  person Greg Schmidt    schedule 24.02.2021
comment
Я думаю, что Грег прав. Пользователь принадлежит к организации, в то время как в организации много пользователей. Пользователь вообще сохраняется при добавлении организации?   -  person cnizzardini    schedule 24.02.2021
comment
@GregSchmidt извините, это опечатка. Я исправил это сейчас. Вы правы, Пользователь делает belongsTo Организацию, и это то, что было определено. Я обновил свой пост.   -  person Andy    schedule 24.02.2021


Ответы (1)


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

Во-первых, $this->TblOrgs->TblUsers — это объект таблицы пользователей, поэтому при использовании

$org = $this->TblOrgs->TblUsers->newEmptyEntity();

что вы делаете, так это создаете новый пользовательский объект. Тот факт, что вы получили доступ к этому объекту таблицы через таблицу orgs и назвали его $org, этого не меняет. Он не создает каким-то волшебным образом пустую организацию с пустой пользовательской сущностью. Но вам здесь вообще не нужна эта структура сущностей, только пустая организационная сущность. Вернитесь к простому:

$org = $this->TblOrgs->newEmptyEntity();

Теперь в вашей форме вам понадобится что-то вроде этого:

<?= $this->Form->create($org) ?>
<?= $this->Form->control('companyname') ?>
<?= $this->Form->control('tbl_users.0.email') ?>
<?= $this->Form->end() ?>

Поле называется tbl_users.0.email, потому что:

  1. Имя таблицы преобразуется в формат подчеркивания нижнего регистра.
  2. Это отношение hasMany от организаций к пользователям, поэтому ожидается массив пользователей; мы должны указать числовой индекс в этом массиве, и 0 — отличное место для начала. Если вы собираетесь одновременно добавить второго пользователя, поле для этого будет tbl_users.1.email.

Примечание. Отличный способ выяснить, в каком формате помощник формы ожидает от вас создания имен полей, — прочитать существующий набор записей из базы данных (в данном случае, организации и ее пользователей), а затем просто выгрузить этот data с чем-то вроде debug($org);. Вы увидите, что $org имеет свойство tbl_users, которое является массивом, и оно указывает прямо на эту структуру, которую я описал выше.

С такими настроенными полями вы сможете вставлять полученные данные непосредственно в ваш объект $org в контроллере и сохранять их без какой-либо дополнительной работы. Патч создаст всю структуру с объектом класса TblOrg со свойством tbl_users, которое представляет собой массив, содержащий один объект класса TblUser, и проверка будет выполнена для обоих из них. (По крайней мере, так и должно быть; вы можете использовать debug($org);, как указано выше, чтобы подтвердить это.) И когда вы сохраните этот объект, он сначала сохранит объект TblOrg, затем добавит этот новый идентификатор в объект TblUser перед его сохранением, а также проверит правила для обоих и убедитесь, что ничего не сохраняется в базе данных, если это не может быть сохранено все. Все это происходит автоматически с помощью одного вызова save!

Если ваша ассоциация была отношением hasOne или ownTo (например, если вы добавляли нового пользователя, а также организацию, в которой он состоит, а не наоборот), вы можете вывести образец $user и увидеть, что он имеет свойство под названием tbl_org, которое является просто сущностью, а не массивом сущностей, и обратите внимание, что tbl_org теперь в единственном числе, потому что это всего лишь одна сущность, а не группа. В этом случае используемое имя поля будет tbl_org.companyname, там вообще нет индекса массива.

person Greg Schmidt    schedule 25.02.2021
comment
Спасибо, это хороший ответ. Одна из вещей, которую я не упомянул (по моей собственной вине), заключается в том, что мы используем некоторые пользовательские методы в классах TblUsers и TblOrgs Table для сохранения данных. Поэтому мы не использовали patchEntity в контроллере. Одна из причин этого заключается в том, что нам нужно было манипулировать данными запроса, в частности, для пользователя, чтобы сохранить некоторые значения по умолчанию, а форма не имеет всех полей, необходимых для пользователя на этапе add(), потому что это средство быстрого добавления, позволяющее сказать кто является владельцем организации. Мы продолжим изучать его, но здесь есть много полезной информации. - person Andy; 25.02.2021
comment
Чтобы манипулировать данными перед сохранением, изучите обратные вызовы beforeMarshal, beforeRules и beforeSave. - person Greg Schmidt; 25.02.2021