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:
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:
- The table name gets converted to lower case underscore format.
- 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.