Using FormProtection in views extending one another

Hi everyone

I’m working on a sign-up page that changes a little depending on the service. So I decided to have

  • a common view that does $this->Form->create() with fields like email, name etc. and
  • views for each service type, having service-specific inputs and extending the common view.

Everything works, but when I enable the FormProtection component, it (understandably) thinks that $this->Form instance in each view is a separate form.

I tried to store a copy of $this->Form in a variable and pass it around, but it still errors out with Unexpected field in POST data. Here’s what I did:

<?php
// common.php
echo $Form->create(null);
echo $Form->control('email');
echo $this->fetch('content');
echo $Form->submit('Start');
echo $Form->end();

debug($this->viewVars);
<?php
// vps.php
$Form = $this->Form;
$this->set('Form', $Form);
$this->set('test_variable', 'hello world');
$this->extend('common');
echo $Form->control('hostname');
echo $Form->control('password');

I can see that both Form and test_variable are there in viewVars in the common view, so passing the variables “up” from the extending view seems to be working, but the system still treats it as a different form.

Is this because of how the Form Protection is built, i.e. it expects everything to be inside the same view, or am I doing something wrong? Is there a better way?

I considered doing elements or cells, but I feel like it will get messy, especially because the view files will be spread across different folders. I’d rather have it all together in that template subfolder, and have one view extend or render another.

I also tried doing something like $this->render('vps'), but it renders the whole thing including the layout. Now, I can of course create a blank layout and have that view use it, but that feels messy too.

I’m on CakePHP 5

Actually, I do use a view cell in that form, but for something else. (The code above is a simplified example.)

So I got no FormProtection errors when passing FormHelper instance from the common view. Conversely, I did get Unexpected field when I used just a generic $this->Form inside that cell.

So passing an instance of FormHelper across different files is not a problem, the problem seems to be extending the views. Maybe it has to do with the order of the events, or where exactly I am calling FormHelper::create()?

I would actually prefer to start a form in the common view and have it somehow picked up in the vps view. I tried:

<?php
// common.php
$Form = $this->Form;
$this->set('Form', $Form);
$this->set('test_variable_from_common', 'hello world');
<?php
// vps.php
$result = $this->extend('common');
debug($this->viewVars); // nothing from common
debug($result->viewVars); // nothing from common either

I think your problem is not that it’s two different Form objects, but the internal state of the Form object at different times. Rendering fields with the Form helper outside of a Form->createForm->end block will not add those fields to the form protection details that Form->end adds to your HTML.

IMO, the best way would be elements. Or, consider whether you even need the form protection on this particular form; disabling it would certainly resolve this.

That’s the first thing I tried, but there are more problems.

I use FriendsOfCake/bootstrap-ui and I call echo $Form->control('my_name') to render an input. What I get out of that differs between the two template files.

In the common file where $Form->create(null) is, it renders the wrapping element for the control with mb-3 class at the bottom, that keeps the spacing right.

In the vps file that extends the common, the control gets rendered without mb-3.

As a result, the whole form looks messed up. Of course I can figure out a way to manually pass the class name, but I feel like the real solution is getting this to work like it’s supposed to.

I looked into the source code (bootstrap-ui/src/View/Helper/FormHelper.php at c73af029694faab855a007462c99542cafbd21c7 · FriendsOfCake/bootstrap-ui · GitHub) and that is exactly the case:

/**
 * Set on `Form::create()` to tell the spacing type.
 *
 * @var string|false|null
 */
protected string|false|null $_spacing = null;

For the form protection component to work correctly, your flow must be calling Form->create exactly once for the whole form, not once in one file and again in another.

Yep, I never intended to do that. While it looks like I indeed ended up having two separate forms, my intention was for it to be the same form shared across templates.

I ended up doing this:

<?php
// signup.php
$this->start('form_start');
    echo $this->Form->create(null);
    $this->set('Form', $this->Form);
$this->end();

$this->extend('signup_'.$serviceCode);
<?php
// signup_vps.php
$this->extend('signup_common');
echo $Form->control('hostname');
<?php
// signup_common.php
echo $this->fetch('form_start')
echo $Form->control('email');
echo $this->fetch('content');
echo $Form->submit('Start');
echo $Form->end();

This way the form gets created in the template that’s rendered first. Then the resulting HTML as well as form helper object are passed down the nested views.

This is messy as well, but at least it works.