2

I'm trying to validate a custom Element in my Craft 3 plugin.

Where should I be putting my validation rules?

I have two classes: one for the Element, one for the ActiveRecord (e.g. Invoice and InvoiceRecord).

One of the requirements is that one of its field (e.g. number) has to be unique.

If I override rules() on the Element, the validation occurs normally but the unique rule throws an error:

Call to undefined method Invoice::getDb()

Now if I try to add the unique rule to my Record's rule() override, it does seem to fail validation and prevent saving the record, but no validation error is thrown (Craft flashes the "saved successfully" message and returns to the element index, but without the new/updated element).

I understand that the record's validation is called only in afterSave, so technically validation has already been done:

public function afterSave(bool $isNew)
{
    if ($isNew) 
    {
        $record = new InvoiceRecord();
        $record->id = $this->id;
    } 
    else 
    {
        $record = InvoiceRecord::findOne($this->id);
    }

    $record->number = $this->number; // Has to be unique

    $record->save();

    parent::afterSave($isNew);
}

But I don't understand how to get my validation working for something rather simple.

I can't find anything about validation in the Craft 3 plugin docs or migration docs from Craft 2, does anybody know?

3

Here's my findings after some experimentation:

  1. Elements and Records are two entirely separate entities. The Element is basically the core Craft database entry, while the Record is the additional database entry in a new table with your custom fields.
  2. Validation should always be done on the element if you use the pattern used in the question, because saveElement() will call validate on the element itself, not the record. If you validate on the record and it fails ($record->save() returning false), the phantom element will still be created, but no record will refer to it. Yikes.
  3. When validating on the element, some rules such as unique require access to the database, which is not bound to the element per se. In that case, you should map the targetClass parameter of the rule to the record class.
  4. However that does mess up same-model detection with UniqueValidator: the validation will fail even if you're editing the current model, saying its unique field is already in use (by itself!).
  5. Thankfully, you can use the filter parameter of the rule to manually ignore the current ID. However, you must make sure not to use that is the ID is null (i.e. when creating a new element), because != does not work with NULL values in SQL (you should be using IS NOT instead).

Summary: here's how to implement unique validation on the Element class:

    $rules[] = 
    [ 
        'myField', 
        'unique',
        'targetClass' => MyRecord::class,
        'filter' => function($query) {
            if ($this->id !== null) {
                $query->andWhere('`id` != :id', [ 'id' => $this->id ] );
            }
        }
    ];

That's it!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.