Attribute Value Objects and Output Formatting

In most examples of working with attributes programmatically, we simply explain how to run a method like

$file->getAttribute('width'); // returns a decimal

or

$page->getAttribute('exclude_nav'); // returns true or false

or

$page->getAttribute('thumbnail'); // returns a Concrete\Core\Entity\File\File object or null

In these examples, we're retrieving attribute values in what we refer to as their native attribute value response. This is the value that it makes the most sense to work with, and which offers the most flexibility. For example, the Image/File attribute could simply return a file ID – but in 99% of the cases when you're working with an Image/File attribute, and retrieving it programmatically, you're going to want to do something with the underlying file object – hence, the native value object format.

The native attribute response is useful for working with objects programmatically, but what if we want to do something like display these attributes on a user profile, or in an email to a customer? Let's say we have an array of attribute keys

$attributes = array($widthKey, $excludeKey, $thumbnailKey);

(This example is a bit contrived, since we're specifying what keys they are – but what if we were pulling this list from the database and didn't know what the attributes were? This is when the example makes more sense.)

If we want to loop through these attributes and display them on a page, how would we do that?

foreach($attributes as $key) {
    $response = $page->getAttribute($key);
}

The $response will be something very different each time. Should we run a switch on the $key type in order to figure out how to process the response?

foreach($attributes as $key) {
    $response = $page->getAttribute($key);
    switch($key->getAttributeTypeHandle()) {
        case 'boolean':
            if ($response) {
                print 'Yes';
            }
            break;
        case 'image_file':
            print $response->getURL();
            break;
        case 'number':
            print $response;
            break;
    }
}

That code is hardly elegant, and only works with attributes we specifically hard-code. Hardly extensible, either.

What we need to do is grab the attribute value object instead of the native response, and use the attribute value object to get the display value for the attribute type. We can do this by calling getAttributeValueObject() instead of getAttribute(). getAttributeValueObject() is guaranteed to return an instance of Concrete\Core\Attribute\AttributeValueInterface (or null.).

$file->getAttributeValueObject('width'); // return Concrete\Core\Entity\Attribute\Value\FileValue

or

$page->getAttributeValueObject('exclude_nav');  // return Concrete\Core\Entity\Attribute\Value\PageValue

or

$page->getAttributeValueObject('thumbnail'); // return Concrete\Core\Entity\Attribute\Value\PageValue

At this point, we have a number of methods available to us. Most of these are defined in Concrete\Core\Entity\Attribute\Value\AbstractValue.

We can get the controller of the attribute, which can be useful:

$controller = $value->getController();

We can get the underlying attribute value object

$fileValue = $file->getAttributeValueObject('width');
$value = $fileValue->getValueObject(); // Returns Concrete\Core\Entity\Attribute\Value\Value\NumberValue

And, most relevant to this discussion, we can get an output response suitable for several contexts.

$thumbnail = $page->getAttributeValueObject('thumbnail');
print $thumbnail->getDisplayValue(); // Outputs <img src="/path/to/image.jpg" /> as a full HTML tag.
print $thumbnail->getPlainTextValue(); // Outputs http://mysite.com/download_file/123
print $thumbnail->getSearchIndexValue() ; // Returns 123

Deprecated – Display Sanitized

In concrete5 5.7 and earlier, there was the concept of 'displaySanitized' values as well as display values. There no longer is such distinction. It is assumed that the display responses of attribute values should be sanitized such that they won't cause mischief when rendered. This means that the display value of a rich text attribute will strip all problematic tags, etc...

Controlling the Attribute Value Response

So how does an attribute type determine the following values?

  • Native Value
  • Display Value
  • Plain Text Value
  • Search Index Value

Native Value

The native response can be defined in two ways. If the attribute type has its own data value object class (e.g. the number attribute type uses Concrete\Core\Entity\Attribute\Value\Value\NumberValue, the image/file attribute type uses Concrete\Core\Entity\Attribute\Value\Value\ImageFileValue, the easiest way is to simply create the method

public function getValue()
{

}

Within the data value entity class itself. For example, the Image/File attribute controls its native response by defining Concrete\Core\Entity\Attribute\Value\Value\ImageFileValue::getValue() as

public function getValue()
{
    return $this->file;
}

However, what if an attribute type extends a core data value object? For example, when we first created our Property Location attribute type, it used the NumberValue object to store its data. But its native value response was its own custom \PropCo\Property\Location object. That's because you can also control the native value response by defining getValue() from within the attribute type controller as well.

public function getValue()
{
    /**
     * @var $value PropertyLocationValue
     */
    $value = $this->attributeValue->getValueObject();
    if ($value) {
        return new Location($value->getPropertyLocationID(), $value->getCustomLabel());
    }
}

Display Value

Display value for an attribute type can be defined in two ways: first you can define getDisplayValue() from within an attribute type controller. First, lets add some methods to the PropCo\Property\Location class to retrieve the name of the location:

public function getPropertyLocationName()
{
    if ($this->customLabel) {
        return $this->customLabel;
    } else {
        switch($this->propertyLocationID) {
            case self::LOCATION_CROWN_PLAZA:
                return 'Crown Plaza';
            case self::LOCATION_TOWN_SQUARE:
                return 'Town Square';
            case self::LOCATION_HILL_ROAD:
                return 'Hill Road';
            case self::LOCATION_UPTOWN_AVENUE:
                return 'Uptown Avenue';
        }
    }
    return '';
}

Now, let's add getDisplayValue support to our Property Location custom attribute:

public function getDisplayValue()
{
    $location = $this->getValue();
    if (is_object($location)) {
        /**
         * @var $location Location
         */
        return $location->getPropertyLocationName();
    }
    return '';
}

That's it! When getDisplayValue() is called on objects of this type, this method will be run.

The display value response can also be specified by implementing a __toString() method on the Concrete\Core\Entity\Attribute\Value\Value\AbstractValue class, if a custom one is used for the attribute type.

Plain Text Value.

The plain text value works exactly the same way as the display value – implementing a __toString() method works here as well – except that the controller method that can control the response in this situation is getPlainTextValue().

Search Index Value

The search index value is the value returned to the Concrete\Core\Attribute\Key\SearchIndexer\StandardSearchIndexer::indexEntry() method. It has to match the value that is expected to go into the columns. An array can also be used. More on this in the attribute search index documentation.)

This can be controlled by defining getSearchIndexValue() within the attribute type controller and returning either a value or an array of key => values.

Loading Conversation