0
votes

I have 2 models Owner and User associated as User BelongsTo Owner.

I have only one Form to collect datas for both models/tables.

the Form layout is declared in my add.ctp below:

<div class="container-fluid">
    <div class="row">
        <div class="col-md-9">
            <h1><?= __("Création d'un nouveau propriétaire") ?></h1>
            <p>&nbsp;</p>
            <div class="col-md-19 col-md-offset-2 container-fluid well">
                <?= $this->Form->create($owner, array('novalidate' => true)) ?>
                    <fieldset>
                        <div class="form-group">
                            <?= $this->Form->input('username', array('label' => __("Nom d'utilisateur :"), 'class' => 'form-control', 'placeholder' => __("Nom d'utilisateur"), 'autocomplete' => 'off')); ?>
                        </div>
                        <div class="form-group">
                            <?=  $this->Form->input('password', array('label' => __("Mot de passe :"), 'class' => 'form-control', 'placeholder' => __("Mot de passe"))); ?>
                        </div>

                        // I cut some fields but username and password belong to User, postcode and city belong to Owner

                        <div class="form-group">
                            <?= $this->Form->input('postcode', array('label' => __("Code postal :"), 'class' => 'form-control', 'placeholder' => __("Code postal"))); ?>
                        </div>
                        <div class="form-group">
                            <?= $this->Form->input('city', array('label' => __("Ville :"), 'class' => 'form-control', 'placeholder' => __("ville"))); ?>
                        </div>

                    </fieldset>
                <?= $this->Form->button(__('Enregistrer')) ?>
                <?= $this->Form->end() ?>
            </div>
        </div>
    </div>
</div>

This form is controlled by OwnersController.php

public function add() {

    $this->loadModel('Users');

    $user = $this->Users->newEntity($this->request->data);
    $owner = $this->Owners->newEntity($this->request->data);

    $addDatas = [ 
            'owner_id' => 'id',
            'email'     => $owner['email'],
            'role'  => 'owner',
            'token'     => md5(time() . '-' . uniqid()),
    ];

    $user = $this->Users->patchEntity($user, $addDatas);

    $owner->users = [$user];

    if ($this->request->is('post')) {
        if ($this->Owners->validate($owner)) {
            if ($this->Owners->save($owner)) {
                $user = $this->Users->get($owner->users[0]['id']);
                $email = new Email('gmail');
                $email->template('activationLink')
                    ->emailFormat('text')
                    ->to($owner['email'])
                        ->from('[email protected]')
                    ->subject(__('Votre inscription sur monsite.com'))
                    ->viewVars(['user' => $user])
                    ->send();

                $this->Flash->success(__("Merci de vous être enregistré. un email a été envoyé à {0} pour activer votre compte", $owner['email']));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__("Impossible de vous enregistrer, veuillez corriger les erreurs"));
        } else {
            $this->Flash->error(__("Impossible de vous enregistrer, veuillez corriger les erreurs"));
        }
    }

    $this->set(compact('owner'));
}

Everything works fine, I can input my data, validate and save Owner's and User's data magically thanks to the fabulous ORM.

But I have a problem with error messages from validators.

No matter that the error comes from Owners's validator or User's validator, I will see my flash message telling something is wrong in the Form, but I only see fields' error messages for Owner's data, not for the User's ones.

If I debug the Owner after I submit a blank Form, I can see that:

object(App\Model\Entity\Owner) {

    'new' => true,
    'accessible' => [
        'email' => true,
        'phone' => true,
        'company' => true,
        'tva_number' => true,
        'address' => true,
        'postcode' => true,
        'city' => true,
        'users' => true
    ],
    'properties' => [
        'company' => '',
        'email' => '',
        'phone' => '',
        'address' => '',
        'postcode' => '',
        'city' => '',
        'users' => [
            (int) 0 => object(App\Model\Entity\User) {

                'new' => true,
                'accessible' => [
                    'username' => true,
                    'password' => true,
                    'first_name' => true,
                    'last_name' => true,
                    'email' => true,
                    'role' => true,
                    'owner' => true,
                    'sites_user' => true,
                    'active' => true,
                    'token' => true
                ],
                'properties' => [
                    'username' => '',
                    'password' => '$2y$10$EtGitvmw7CeIOJOeA4eNKuZBifd97f165O9I5gidpiC7p1NYRGsE.',
                    'first_name' => '',
                    'last_name' => '',
                    'email' => '',
                    'role' => 'owner',
                    'token' => '4f842f347e06aff9a3778199c9a8d20a'
                ],
                'dirty' => [
                    'username' => true,
                    'password' => true,
                    'first_name' => true,
                    'last_name' => true,
                    'email' => true,
                    'role' => true,
                    'token' => true
                ],
                'original' => [],
                'virtual' => [],
                'errors' => [
                    'username' => [
                        (int) 0 => 'Vous devez indiquer un nom d'utilisateur'
                    ],
                    'first_name' => [
                        (int) 0 => 'Vous devez indiquer votre prénom'
                    ],
                    'last_name' => [
                        (int) 0 => 'Vous devez indiquer votre nom'
                    ],
                    'email' => [
                        (int) 0 => 'This field cannot be left empty'
                    ]
                ],
                'repository' => 'Users'

            }
        ]
    ],
    'dirty' => [
        'company' => true,
        'email' => true,
        'phone' => true,
        'address' => true,
        'postcode' => true,
        'city' => true,
        'users' => true
    ],
    'original' => [],
    'virtual' => [],
    'errors' => [
        'email' => [
            (int) 0 => 'Vous devez saisir votre email'
        ],
        'phone' => [
            (int) 0 => 'Vous devez saisir votre téléphone'
        ],
        'address' => [
            (int) 0 => 'Vous devez saisir votre adresse'
        ],
        'postcode' => [
            (int) 0 => 'Vous devez saisir votre code postal'
        ],
        'city' => [
            (int) 0 => 'vous devez saisir votre ville'
        ]
    ],
    'repository' => 'Owners'

}

As you can see, the object contain errors about both Owner and associated User but it seems that cakePHP cannot get errors from Users for a reason I can't guess.

So I wonder if it should work and if I made a mistake somewhere which could explain that it cannot retrieve User's errors.

2
Wasn't this solved on IRC already? Your field names are lacking the association: book.cakephp.org/3.0/en/views/helpers/… - ndm
Unfortunately not ndm. As I wasn't not so sure to understand the doc, I tried User.username, Users.username, also $user.username after adding user to $this->set(compact('owner', 'user')); nothing works. But I'm sure there is something I didn't understand here. - fralbo
And in fact I don't understand why inputs perfectly work (setting and getting values) without giving more infos to input() while error messages don't. I wonder if the problem doesn"t come from other thing. - fralbo

2 Answers

1
votes

As already mentioned, the main problem here is that your inputs are lacking the appropriate associations.


Request data is checked before entities

The fact that the inputs pick up the entered data (which is probably what is confusing you judging by your comments) has nothing to do with what is set or not set on the entity!

This is due to how the entity context works (contexts are the layers that sit between the form helper and data), before it tries to check the entity for data, it will check the request, and this is where it picks up for example the username field, if it would check for that in the entity, then it wouldn't find it, as it would look for it on the Owner entity due to the lack of the association in the field name.

See https://github.com/cakephp/cakephp/blob/3.0.0-beta3/src/View/Form/EntityContext.php#L201-L204


Validation errors are only to be found on the entity

So while your inputs can pick up the data, as it is stored in the request with the "wrong" fieldname, validation errors can only be found on the entity, and this is where things finally go wrong with your code, the Owner entity has no username field, but that is where the entity context will look for the error.

See https://github.com/cakephp/cakephp/blob/3.0.0-beta3/src/View/Form/EntityContext.php#L468

It won't find anything there, as the error is located in the first User entity, stored in the Owner entities users property (Owner->users[0]->errors), so in order to be able to pick up these errors, your fieldname must look like

users.0.username

which reflects the structure in the entity.

See http://book.cakephp.org/3.0/en/views/helpers/form.html#field-naming-conventions


Creating a new main entity is all you need for associated models

In your controller, you are creating a User entity as well as a Owner entity, this isn't necessary since your models are associated, the newEntity() call on the Owners table will create everything that is needed, an Owner entity with its data and all it's associated entites data, that is the User entity.

See http://book.cakephp.org/3.0/en/orm/table-objects.html#converting-request-data-into-entities

If you need to set additional data to the User entity, simply patch the entity that was created and placed in the Owner entities users property, something like this:

public function add() {
    $this->loadModel('Users');

    // Creates an entity with all necessary data
    $owner = $this->Owners->newEntity($this->request->data);

    if ($this->request->is('post')) {
        if(isset($owner->users[0])) {
            // Patch the existing (new) entity with some 
            // additional data
            $this->Users->patchEntity($owner->users[0], [
                'email' => $owner->email,
                'role'  => 'owner',
                'token' => md5(time() . '-' . uniqid()),
            ]);
        } else {
            // Bail out here or check for the existence of
            // a user using validation.
            //
            // You should have some kind of validation somewhere
            // in order to make sure that a user (only 
            // one (1)) was submitted, and the controller
            // mostly isn't really the right place for such logic
        }

        if ($this->Owners->save($owner)) {
            $this->Flash->success(__("Merci de vous être enregistré. un email a été envoyé à {0} pour activer votre compte", $owner['email']));
            return $this->redirect(['action' => 'index']);
        } else {
            $this->Flash->error(__("Impossible de vous enregistrer, veuillez corriger les erreurs"));
        }
    }

    $this->set(compact('owner'));
}

Note that I've removed the validate() and the get() call from the example, explicitly invoking validation is not necessarily necessary, as per your comments you probably don't need it, you'd now if you would, and if you would, then you'd disable validation that is triggered by the save() call, because validating twice shouldn't be necessary.

See http://book.cakephp.org/3.0/en/orm/table-objects.html#validating-entities

The example should work fine, but make sure that you've read the section about how to avoid mass assigment attacks!

0
votes

You can easily kick your errors to the view using the set method

if ($Models->errors()) {
$errors=$Models->errors();
 $this->set(compact('errors'));

}