Skip to content
Advertisement

CakePHP 4 – how to validate forms that need to save data to multiple tables

Apologies if this has been asked before. All of the examples I can find are old or apply to legacy versions of CakePHP, e.g. cakephp: saving to multiple models using one form is 7 years old.

I have an application in CakePHP 4.1.6. Two of the tables in the database are called tbl_users and tbl_orgs (“orgs” in this case means “Organisations”).

When I add an Organisation I also want to create a User who is the main contact within the Organisation. This involves saving to both the tbl_orgs and tbl_users tables when the form is submitted.

The problem I’m experiencing is how to get the form working in a way where it will run the validation rules for both tbl_users and tbl_orgs when submitted.

This is how our application is currently structured:

There is a Controller method called add() in src/Controller/TblOrgsController.php. This was generated by bake and was initially used to insert a new Organisation into the tbl_orgs table. At this point it didn’t do anything in terms of tbl_users however it worked in terms of saving a new Organisation and running the appropriate validation rules.

One validation rule is that every companyname record in tbl_orgs must be unique. If you try to insert more than 1 company with the name “My Company Limited” it would give the validation error “This company name already exists”:

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

Whilst the above applies to TblOrgs we also have an buildRules() in TblUsers which applies similar logic on an email field to make sure that all email addresses are unique per user.

In the add() Controller method we start by specifying a new empty entity for TblOrgs:

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

When the form is created we pass $org:

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

When the TblOrgs fields are rendered by the browser we can inspect the HTML and see these are obeying the corresponding Model. This is clear because of things such as required="required" and maxlength="100" which correspond to the fact that field is not allowed to be empty and is a VARCHAR(100) field in the database:

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

It also works in terms of the rules specified in buildRules for TblOrgs. For example if I enter the same company name twice it shows the appropriate error in-line:

enter image description here

I then tried to introduce fields for TblUsers. I prefixed the form fields with dot notation, e.g. this is intended to correspond to tbl_users.email input field:

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

When inspecting the HTML it doesn’t do the equivalent as for TblOrgs. For example things like maxlength or required are not present. It effectively isn’t aware of TblUsers. I understand that $org in my Controller method is specifying a new entity for TblOrgs and not TblUsers. I reviewed the CakePHP documentation on Saving With Associations which says

The save() method is also able to create new records for associations

However, in the documentation the example it gives:

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

In this case Tags is a different Model to Comments but newEmtpyEntity() works for both. With this in mind I adapted my add() method to become:

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

But this now gives an Entity for TblUsers. It seems you can have either one or the other, but not both.

The reason this doesn’t work for my use-case is that I can either run my Validation Rules for TblOrgs (but not TblUsers) or vice-versa.

How do you set this up in a way where it will run the validation rules for both Models? It doesn’t seem to be an unreasonable requirement that a form may need to save data to multiple tables and you’d want the validation rules for each of them to run. I get the impression from the documentation that it is possible, but it’s unclear how.

For reference there is an appropriate relationship between the two tables:

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

and

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

Advertisement

Answer

Okay, lots of confusion to clear up here. 🙂 My assumption here, based on what you’ve written, is that you’re trying to use a single form to add a new organization, and the first user in it, and then maybe later you’ll add more users to the org.

First, $this->TblOrgs->TblUsers is your users table object, so when you use

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

what you’re doing is creating a new user entity. The fact that you got to that table object through the orgs table, and that you’re calling it $org doesn’t change that. It doesn’t somehow magically create a blank org entity with a blank user entity in it. But you don’t need that entity structure here at all here, just the empty org entity. Go back to simply:

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

Now, in your form, you’ll want something like this:

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

The field is called tbl_users.0.email because:

  1. The table name gets converted to lower case underscore format.
  2. It’s a hasMany relation from orgs to users, so it’s expecting an array of users; we have to give a numeric index into that array, and 0 is a great place to start. If you were going to add a second user at the same time, the field for that would be tbl_users.1.email.

Note: A great way to figure out what format the form helper is expecting you to create your field names in is to read an existing set of records from the database (in this case, an org and its users), and then just dump that data, with something like debug($org);. You’ll see that $org has a property called tbl_users, which is an array, and that will point straight to this structure I’ve described above.

With the fields set up like this, you should be able to patch the resulting data directly into your $org entity in your controller, and save that without any other work. The patch will created the entire structure, with a entity of class TblOrg, with a tbl_users property which is an array containing a single entity of class TblUser, and validation will have been done on both of them. (At least it should; you can use debug($org); as mentioned above to confirm it.) And when you save this entity, it will first save the TblOrg entity, then add that new ID into the TblUser entity before saving it, as well as checking the rules for both and making sure that nothing gets saved to the database if it can’t all be saved. That all happens automatically for you with the single save call!

If your association was a hasOne or belongsTo relation (for example if you were adding a new user and also the org that they’re in, instead of the other way around), you could dump a sample $user, and see that it has a property called tbl_org which is just a straight-up entity, not an array of entities, and note that tbl_org is now singular, because it’s just one entity instead of a bunch. In this case, the field name to use would be tbl_org.companyname, no array index in there at all.

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement