Validating Attribute Keys and Values

For complex attribute types, it can be important to ensure that they are validated before saving to the database in a potentially compromised state. This is true for both keys and values. You don't want an image/file attribute to be saved without a file, for example – and if you're creating a new attribute key with certain settings, you might not want them to be saved without those settings completely filled out.

So how do we add attribute key and value validation? It's easy.

Validating Attribute Keys

Let's take our Property Location attribute and add validation to its key. If you recall, our attribute key has a custom select menu that lets you choose the format, from either select menu or a radio list. Let's make it so that you're forced to actively choose. First, add an empty option to the type_form.php select menu:

<fieldset>
    <legend><?= t('Location Settings') ?></legend>

    <div class="form-group">
        <label class="control-label" for="formDisplayMethod">Form Display Method</label>
        <select class="form-control" name="formDisplayMethod">
            <option value="">** Choose a Display Method</option>
            <option value="select"
                    <?php if (isset($formDisplayMethod) && $formDisplayMethod == 'select') { ?>selected<?php } ?>>Select
                Menu
            </option>
            <option value="radio_list"
                    <?php if (isset($formDisplayMethod) && $formDisplayMethod == 'radio_list') { ?>selected<?php } ?>>
                Radio Button List
            </option>
        </select>
    </div>
</fieldset>

Now, it's possible for someone to submit the form without selecting the form factor. So let's implement a validateKey() method in our controller. This will return an instance of the Concrete\Core\Error\ErrorList\ErrorList object.

public function validateKey($data = false)
{
    $e = $this->app->make('error');
    if (!isset($data['formDisplayMethod']) || !in_array($data['formDisplayMethod'], array(
            'select', 'radio_list'))) {
        $e->add(t('You must specify a display method.'));
    }

    return $e;
}

That's it! Now if a proper option isn't selected in the select menu, the key will not be able to be created or updated.

Validating Attribute Value Forms

Validating an attribute's form submission is a similar process. Let's add validation to our Property Location attribute. First, let's add "None" as an option (but we won't ultimately let you choose it.)

<?php if (isset($formDisplayMethod) && $formDisplayMethod == 'radio_list') { ?>

    <div class="radio">
        <label><input <?php if (!isset($propertyLocationID) || !$propertyLocationID) { ?>checked<?php }  ?> type="radio" name="<?=$view->field('propertyLocationID')?>" value=""> None Chosen </label>
    </div>

    <div class="radio">
        <label><input <?php if (isset($propertyLocationID) && $propertyLocationID == 1) { ?>checked<?php }  ?> type="radio" name="<?=$view->field('propertyLocationID')?>" value="1"> Crown Plaza</label>
    </div>
    <div class="radio">
        <label><input <?php if (isset($propertyLocationID) && $propertyLocationID == 2) { ?>checked<?php }  ?> type="radio" name="<?=$view->field('propertyLocationID')?>" value="2"> Town Square</label>
    </div>
    <div class="radio">
        <label><input <?php if (isset($propertyLocationID) && $propertyLocationID == 3) { ?>checked<?php }  ?> type="radio" name="<?=$view->field('propertyLocationID')?>" value="3"> Hill Road</label>
    </div>
    <div class="radio">
        <label><input <?php if (isset($propertyLocationID) && $propertyLocationID == 4) { ?>checked<?php }  ?> type="radio" name="<?=$view->field('propertyLocationID')?>" value="4"> Uptown Avenue</label>
    </div>

<?php } else { ?>

    <select class="form-control" name="<?=$view->field('propertyLocationID')?>">
        <option value="">Select a Location</option>
        <option value="1" <?php if (isset($propertyLocationID) && $propertyLocationID == 1) { ?>selected<?php }  ?>>Crown Plaza</option>
        <option value="2" <?php if (isset($propertyLocationID) && $propertyLocationID == 2) { ?>selected<?php }  ?>>Town Square</option>
        <option value="3" <?php if (isset($propertyLocationID) && $propertyLocationID == 3) { ?>selected<?php }  ?>>Hill Road</option>
        <option value="4" <?php if (isset($propertyLocationID) && $propertyLocationID == 4) { ?>selected<?php }  ?>>Uptown Avenue</option>
    </select>

<?php } ?>

<div class="form-inline">
    <label><?=t('Custom Label')?></label>
    <input class="form-control" type="text" value="<?=$customLabel?>" name="<?=$view->field('customLabel')?>">
</div>

Now, let's make it so that you're forced to choose a valid property location ID, by adding validateForm($data) to the Property Location attribute type controller. First lets import some classes that we're going to need at the top of our controller:

use Concrete\Core\Error\ErrorList\Error\Error;
use Concrete\Core\Error\ErrorList\Error\FieldNotPresentError;
use Concrete\Core\Error\ErrorList\Field\AttributeField;

Then, implement the method like this.

public function validateForm($data)
{
    if (!isset($data['propertyLocationID']) || !$data['propertyLocationID']) {
        return new FieldNotPresentError(new AttributeField($this->getAttributeKey()));
    }
    $propertyLocationID = intval($data['propertyLocationID']);
    if (!in_array($propertyLocationID, array(1, 2, 3, 4))) {
        return new Error(t('Invalid property location ID.'), new AttributeField($this->getAttributeKey()));
    }
    return true;
}

Let's walk through what's happening here. First, we check to see if the propertyLocationID is contained within the request at all. If it's not, we throw a field not present error. Then, if we get further down, we check the validity of the contents of the request. If it's not one of our valid property location IDs (1, 2, 3 or 4), we throw a more specific error. Finally, we return true. Important: If you don't return true at the end of validateForm it is assumed validation has failed.

So what's the deal with these different error methods? There are times when many attributes are validated at once, but if they're not present, they are skipped. For example, when working with the Image/File attribute, if the attribute value is already set and no new file is included in the request, validation is not assumed to have failed. However, if the file provided is invalid, a specific failure is desired.

Validating Attributes in Current State, Not Form Submission

The above example deals with validating attributes that are being submitted, whether through a user creation process, page composer, or some other form submission. Sometimes, however, attributes need to be validated against their current value objects. For example, let's say we create a page draft that, according to page type rules, must have a thumbnail object associated with it. We fill out all the content on the page, and attempt to publish the page, but not through a single unified form submission in composer. We need to loop through all the attributes attached to the page draft and validate their current value objects, to ensure that they pass muster. Here's how we can do that with our Property Location attribute.

public function validateValue()
{
    $location = $this->getValue();
    if (!is_object($location)) {
        return new Error(t('Property Location missing.'), new AttributeField($this->getAttributeKey()));
    }
    if (!$location->getLocationID()) {
        return new Error(t('Invalid property location ID.'), new AttributeField($this->getAttributeKey()));
    }
    return true;
}

That's it! Now you can validate attribute keys and values in multiple contexts.