Problems of validation with Doctrine and laminas form

HI. I tried to update a document of MongoDB I use the Doctrine Module for Laminas and I follow this example in the Doctrine module documentation A Complete Example using Laminas\Form - Doctrine Doctrine Laminas Hydrators.

When I tried to update the document with embed subdocuments the validation failed, It works fine when I create a new document.

This is my updated code method:

    public function updateAccountWithForm(string $vendorAccountId, array $data): array
    {
        $form = new UpdateAccountForm($this->dm);
        $account = $this->dm->getRepository(Account::class)->findOneBy(['vendorAccountId' => $vendorAccountId]);
        if (is_null($account)) {
            return [
                'message' => 'Vendor Account is not found',
                'statusCode' => 411,
            ];
        }

        $form->bind($account);
        $form->setData($data);
        if ($form->isValid()) {
            try {
                $this->dm->flush();
                error_log("[" . __LINE__ . "]:" . __CLASS__ . ", account updated:");
                $response['status'] = true;
                $response['vendorAccountId'] = $vendorAccountId;
                $response['companyName'] = $account->getCompany()->getCompanyName();

            } catch (MongoDBException $e) {
                error_log("[" . __LINE__ . "]:" . __CLASS__
                    . ", account error form:" . $e->getMessage()
                );
                $response['errorMessage'] = $e->getMessage();
            }

        } else {
            $data = $form->getData();
            print_r(['error'=> $data]);
            $messages = $form->getMessages();
            error_log("[" . __LINE__ . "]:" . __CLASS__ . ", invalid form:");
            $response['statusCode'] = 400;
            $response['errorMessage'] = 'validation error';
            $response['errors'] = $messages;
        }

        return $response;
    }

When I populate the form with de existing values ($account) with bind() this works fine but when I do setData all data is missed and I only have the data that I set with setData.

My UpdateAccountForm.php

class UpdateAccountForm extends Form
{
    public function __construct(DocumentManager $dm)
    {
        parent::__construct('update-account-form');
        $this->setHydrator(new DoctrineHydrator($dm));

        $accountFieldset = new AccountFieldset($dm);
        $accountFieldset->setUseAsBaseFieldset(true);
        $this->add($accountFieldset);

    }
}

AccountFieldset.php

class AccountFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(DocumentManager $dm)
    {
        parent::__construct('account');
        $this->setHydrator(new DoctrineHydrator($dm))
            ->setObject(new Account());

        $company = new CompanyFieldset($dm);
        $company->setName('company');
        $this->add($company);
        $this->add([
            'type' => Text::class,
            'name' => 'realm',
        ]);
        $this->add([
            'type' => Text::class,
            'name' => 'domain',
        ]);

    }

    public function getInputFilterSpecification(): array
    {
        return [
            "realm"     => ['required' => false],
            "domain"    => ['required' => false],
        ];
    }
}

CompanyFieldset.php

class CompanyFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(DocumentManager $dm)
    {
        parent::__construct('company');
        $this->setHydrator(new DoctrineHydrator($dm))
            ->setObject(new Company());

        $this->add([
            'type' => Text::class,
            'name' => 'companyId',
        ]);
        $this->add([
            'type' => Text::class,
            'name' => 'companyVersion',
        ]);
        $this->add([
            'type' => Text::class,
            'name' => 'companyName',
        ]);

        $address = new AddressLocationFieldset($dm);
        $address->setName('address');
        $this->add($address);

        $this->add([
            'type' => Email::class,
            'name' => 'emailAddress',
        ]);
        $this->add([
            'type' => Text::class,
            'name' => 'phoneNumber',
        ]);
        $this->add([
            'type' => Url::class,
            'name' => 'websiteUrl',
        ]);
        $this->add([
            'type' => Text::class,
            'name' => 'creationDate',
        ]);
    }

    public function getInputFilterSpecification(): array
    {
        return [
            "companyId"         => ['required' => true],
            "companyVersion"    => ['required' => true],
            "companyName"       => ['required' => true],
            "emailAddress"      => ['required' => false],
            "phoneNumber"       => ['required' => false],
            "websiteUrl"        => ['required' => false],
            "creationDate"      => ['required' => false],
        ];
    }
}

I found this in the documentation:

This said:

One such change is that if a collection is found in a form, but has no associated data, an empty array is assigned to it, even when not in the validation group. This effectively wipes out the collection data when you bind values.

Currently, I have a validation group in createForm.

Example: I want to edit only the field then I send the field.

[account] => Array
(
    [company] => Array
        (
            [companyId] => bcos1.company.0e68afc9-9f53-3ee4-a94f-2cf7b8e1701c
            [companyVersion] => 2
        )

)

But I have an error with the validation because in the company the required fields are: companyId, companyVersion, and companyName; and I send only two required fields but the other one is in the current entity.
The data when I use the bind method ($account):

Array
(
    [account] => Array
        (
            [id] => 62795cc9dcb19701ea757442
            [vendorAccountId] => vendor-account-id-62795cc9227b0
            [company] => Array
                (
                    [companyId] => bcos1.company.5c966432-1a56-37fb-846c-8de8157470e9
                    [companyVersion] => 1
                    [companyName] => Sporer Group
                    [address] => Array
                        (
                            [streetAddress] => 70110 Stokes Locks Apt. 369
                            [city] => Kochchester
                            [region] => Delaware
                            [country] => US
                            [postalCode] => 75178-6851
                            [storeName] => Brekke LLC PLC
                            [coordinates] => Array
                                (
                                    [latitude] => 45.99282
                                    [longitude] => 45.99282
                                )

                        )

                    [emailAddress] => clifton.hoppe@turner.com
                    [phoneNumber] => (729) 995-0270
                    [websiteUrl] => http://hahn.com/iste-repellat-sunt-voluptates-sit
                    [creationDate] => DateTime Object
                        (
                            [date] => 2022-05-09 18:26:17.000000
                            [timezone_type] => 3
                            [timezone] => UTC
                        )

                )

            [realm] => NAA
            [domain] => beta
            [status] => PENDING
            [createdAt] => DateTime Object
                (
                    [date] => 2022-05-09 18:26:17.141000
                    [timezone_type] => 3
                    [timezone] => UTC
                )

        )

)

How I fix this and that pass the validation?

Sorry for the long post and thank you in advance for your help.

You may want to take a look at the following in Fieldset.php

public function bindValues(array $values = [], ?array $validationGroup = null)

Hi @Tyrsson thank you for your response.

I tried your suggestions and still don’t work.

This is my method code:

   $form = new UpdateCompanyForm($this->dm);
        $account = $this->dm->getRepository(Account::class)->getAccount($vendorAccountId);
        if (is_null($account)) {
            return [
                'message' => 'Vendor Account is not found',
                'statusCode' => 411,
            ];
        }

        $form->bindValues($account['company']);
        $form->setData($data);
        if ($form->isValid()) {
            try {
                $this->dm->flush();
                error_log("[" . __LINE__ . "]:" . __CLASS__ . ", account updated:");
                $response['status'] = true;
                $response['vendorAccountId'] = $vendorAccountId;
                $response['companyName'] = $account->getCompany()->getCompanyName();

            } catch (MongoDBException $e) {
                error_log("[" . __LINE__ . "]:" . __CLASS__
                    . ", account error form:" . $e->getMessage()
                );
                $response['errorMessage'] = $e->getMessage();
            }

        } else {
            $messages = $form->getMessages();
           print_r($messages);
            error_log("[" . __LINE__ . "]:" . __CLASS__ . ", invalid form:");
            exit(print_r($form->getData()));
        }

        return $response;
    }

Printing values

input data:
Array
(
    [company] => Array
        (
            [companyId] => bcos1.company.kkkkk-0000
            [companyVersion] => 2
        )

)

error validation:
Array
(
    [company] => Array
        (
            [companyName] => Array
                (
                    [isEmpty] => Value is required and can't be empty
                )

        )

)

Only have the input data setting but the current values are missing
Array
(
    [company] => Array
        (
            [companyId] => bcos1.company.kkkkk-0000
            [companyVersion] => 2
            [companyName] => 
            [emailAddress] => 
            [phoneNumber] => 
            [websiteUrl] => 
            [creationDate] => 
            [address] => Array
                (
                    [streetAddress] => 
                    [city] => 
                    [region] => 
                    [country] => 
                    [postalCode] => 
                    [storeName] => 
                    [coordinates] => Array
                        (
                            [latitude] => 
                            [longitude] => 
                        )

                )

        )

)

Thank you again.

Why are you calling setData($data) after calling bindValues($dataToBind); ?

Hi @Sirpyerre,

I think you’ve mixed two different examples as one. I may be wrong here. But when using doctrine, you don’t use “bindValues()” method. You simple use “bind()” method as shown in the doctrine example. Also, I’m assuming that your “$data” variable contains the data of the form posted.

public function createAction()
{
    // Create the form and inject the EntityManager
    $form = new CreateBlogPostForm($this->entityManager);

    // Create a new, empty entity and bind it to the form
    $blogPost = new BlogPost();
    $form->bind($blogPost);

    if ($this->request->isPost()) {
        $form->setData($this->request->getPost());

        if ($form->isValid()) {
            $objectManager->persist($blogPost);
            $objectManager->flush();
        }
    }

    return ['form' => $form];
}

Thanks!

Actually, I was not referring to an example. By reading bindValues one would think since it hydrates the bound object that would be sufficient. If I am not misunderstanding. The original poster did not have a problem creating it. The issue came when he was trying to run the update. Of course this assumes that the doctrine hydrator would expose a hydrate method to be called. Since the issue was that he is only sending one field, and since bindValues checks, for dsiabled fields, he could disable the other fields and the call to bindValues should pull the values from the entity for hydrating the object.
Or I thought anyway. As long as it works, it works :slight_smile:

Thank you, everyone.

Continue with the same issue.
I tried this way with the bind() method:

public function updateAccountWithForm(string $vendorAccountId, array $data): array
    {
        $form = new UpdateCompanyForm($this->dm);

        $account = $this->dm->getRepository(Account::class)->findOneBy(['vendorAccountId' => $vendorAccountId]);
        if (is_null($account)) {
            return [
                'message' => 'Vendor Account is not found',
                'statusCode' => 411,
            ];
        }
        
        $form->bind($account->getCompany());
        $form->setData($data);
        if ($form->isValid()) {
            try {
                $this->dm->flush();
                error_log("[" . __LINE__ . "]:" . __CLASS__ . ", account updated:");
                $response['status'] = true;
                $response['vendorAccountId'] = $vendorAccountId;
                $response['companyName'] = $account->getCompany()->getCompanyName();

            } catch (MongoDBException $e) {
                error_log("[" . __LINE__ . "]:" . __CLASS__
                    . ", account error form:" . $e->getMessage()
                );
                $response['errorMessage'] = $e->getMessage();
            }

        } else {
            $messages = $form->getMessages();
           print_r($messages);
            error_log("[" . __LINE__ . "]:" . __CLASS__ . ", invalid form:");
            exit(print_r($form->getData()));
        }

        return $response;
}

I can see is setting the current data whit bind

App\Document\Entity\Company Object
(
    [companyId:App\Document\Entity\Company:private] => mzn5.bcos9.company.1806cf97-a756-4fbf-9081-fc162cdffd749
    [companyVersion:App\Document\Entity\Company:private] => 1
    [companyName:App\Document\Entity\Company:private] => petear-glad fc162cdffd789 lm-peter-0004
    [address:App\Document\Entity\Company:private] => App\Document\Entity\Address Object
        (
            [streetAddress:App\Document\Entity\Address:private] => 300 Boren Ave
            [city:App\Document\Entity\Address:private] => Seattle
            [region:App\Document\Entity\Address:private] => US-WA
            [country:App\Document\Entity\Address:private] => US
            [postalCode:App\Document\Entity\Address:private] => 98109
            [storeName:App\Document\Entity\Address:private] => Seattle Storekkk
            [coordinates:App\Document\Entity\Address:private] => App\Document\Entity\Coordinates Object
                (
                    [latitude:App\Document\Entity\Coordinates:private] => 45.99282
                    [longitude:App\Document\Entity\Coordinates:private] => 45.99282
                )

        )

    [emailAddress:App\Document\Entity\Company:private] => johndoe@amazon.com
    [phoneNumber:App\Document\Entity\Company:private] => 1234567890
    [websiteUrl:App\Document\Entity\Company:private] => delivery.com
    [creationDate:App\Document\Entity\Company:private] => DateTime Object
        (
            [date] => 2022-03-06 21:00:52.222000
            [timezone_type] => 3
            [timezone] => UTC
        )

)

I want to update with the following data:

Array
(
    [company] => Array
        (
            [companyId] => bcos1.company.kkkkk-00009
            [companyVersion] => 2
        )
)

And the error validation is:

Array
(
    [company] => Array
        (
            [companyName] => Array
                (
                    [isEmpty] => Value is required and can't be empty
                )
        )
)

As I like said before I only want to update two fields (companyId and companyVersion) and the missing field (companyName) already exists in the current entity. I don’t know if I’m sending the incorrect way.

With the bindValues method, the entity is empty with only de fields that I’m sending (companyId and companyVersion). I show you the code:

 public function updateAccountWithForm(string $vendorAccountId, array $data): array
    {
        $form = new UpdateCompanyForm($this->dm);

        $account = $this->dm->getRepository(Account::class)->getAccount($vendorAccountId);
        if (is_null($account)) {
            return [
                'message' => 'Vendor Account is not found',
                'statusCode' => 411,
            ];
        }
        
        $form->setData($data);
        $form->bindValues($account['company']);
        if ($form->isValid()) {
            try {
                $this->dm->flush();
                error_log("[" . __LINE__ . "]:" . __CLASS__ . ", account updated:");
                $response['status'] = true;
                $response['vendorAccountId'] = $vendorAccountId;
                $response['companyName'] = $account->getCompany()->getCompanyName();

            } catch (MongoDBException $e) {
                error_log("[" . __LINE__ . "]:" . __CLASS__
                    . ", account error form:" . $e->getMessage()
                );
                $response['errorMessage'] = $e->getMessage();
            }

        } else {
            $messages = $form->getMessages();
           print_r($messages);
            error_log("[" . __LINE__ . "]:" . __CLASS__ . ", invalid form:");
            exit(print_r($form->getData()));
        }

        return $response;
    }

The difference with bind method is that bindValues receive an array, but I can’t see the current values $form->getData() that I’m setting and only have the two updates fields.

Array
(
    [company] => Array
        (
            [companyId] => bcos1.company.kkkkk-00009
            [companyVersion] => 2
        )

)
Array
(
    [company] => Array
        (
            [companyName] => Array
                (
                    [isEmpty] => Value is required and can't be empty
                )

        )

)
Array
(
    [company] => Array
        (
            [companyId] => bcos1.company.kkkkk-00009
            [companyVersion] => 2
            [companyName] => 
            [emailAddress] => 
            [phoneNumber] => 
            [websiteUrl] => 
            [creationDate] => 
            [address] => Array
                (
                    [streetAddress] => 
                    [city] => 
                    [region] => 
                    [country] => 
                    [postalCode] => 
                    [storeName] => 
                    [coordinates] => Array
                        (
                            [latitude] => 
                            [longitude] => 
                        )

                )

        )

)

Thank you for your support.

Dear @Sirpyerre,

Your problem refers to changing the relation of the data. Your current account is bound to a company “mzn5.bcos9.company.1806cf97-a756-4fbf-9081-fc162cdffd749” and you want to change it to “bcos1.company.kkkkk-00009”.

So, the change would be

$newCompanyObject = $this->entityManager->getRepository(Company::class);
$newCompanyObject = $newCompanyObject->findOneById('bcos1.company.kkkkk-00009');
$account->setCompany($newCompanyObject);

$this->dm->persist($account);
$this->dm->flush();

I hope it helps.