Creating Attribute Types and Extending Core Attribute Types
Extending a Core Attribute Type
Imagine you're making a website for a property company with four locations: Crown Plaza, Town Square, Hill Road, and Uptown Avenue. You need a way to link users and pages to these locations.
Location Object
We created a custom object for these locations at PropCo\Property\Location
. This object represents the locations, and we want to use it with attributes, not just their ID numbers.
namespace PropCo\Property;
class Location
{
protected $propertyLocationID;
const LOCATION_CROWN_PLAZA = 1;
const LOCATION_TOWN_SQUARE = 2;
const LOCATION_HILL_ROAD = 3;
const LOCATION_UPTOWN_AVENUE = 4;
public function __construct($propertyLocationID)
{
$this->propertyLocationID = $propertyLocationID;
}
}
We'll make a new attribute type called "property_location" based on the number attribute. This lets us store location numbers easily.
Create the Controller
Start by making a controller.php
file at application/attributes/property_location/controller.php
. This is where custom attribute types are stored. The file should start with this basic setup:
namespace Application\Attribute\PropertyLocation;
class Controller extends \Concrete\Attribute\Number\Controller
{
}
Add the Attribute Type
Go to /dashboard/system/attributes/types
on your site. Click Install at the bottom where custom attribute types are listed. If there's an error or nothing shows up, turn off overrides caching at /dashboard/system/optimization/cache
.
Remember to assign the attribute type to categories or you can't use it.
Add an Attribute Key
Now, create a 'property_location' attribute key for pages. Choose property location as the type and set up the key.
Next, we'll work on controller methods and attribute templates. Our attribute will act like a number attribute if we don't add custom parts.
Icon
First, we choose an icon. For property location, a house icon fits. We'll use "home" from Font Awesome.
Add this at the top of your controller:
use Concrete\Core\Attribute\FontAwesomeIconFormatter;
Then, add the getIconFormatter
method:
public function getIconFormatter()
{
return new FontAwesomeIconFormatter('home');
}
This sets the icon for our attribute.
Form
For the attribute form on a page, we want a menu with our four locations. We'll need a form()
method in the controller and a form.php
template in application/attributes/property_location/
.
The form()
method can be empty to start. In form.php
, add this code for the select menu:
<select class="form-control" name="<?=$view->field('value')?>">
<option value="">Select a Location</option>
<option value="1">Crown Plaza</option>
<option value="2">Town Square</option>
<option value="3">Hill Road</option>
<option value="4">Uptown Avenue</option>
</select>
The <?=$view->field('value')?>
part ensures the form works correctly, even with multiple similar attributes on one page.
Now, when you add this attribute to a page, you'll see a neat selection menu.The selected location's ID is saved automatically.
Edit Form
To select a property location in the edit page form, add this to the form()
method:
public function form()
{
if (is_object($this->attributeValue)) {
$value = $this->attributeValue->getValue();
$this->set('propertyLocationID', $value);
}
}
Modify the form template:
<select class="form-control" name="<?=$view->field('value')?>">
<option value="">Select a Location</option>
<!-- Repeat for each location -->
<option value="1" <?php if (isset($propertyLocationID) && $propertyLocationID == 1) { ?>selected<?php } ?>>Crown Plaza</option>
<!-- ... other options ... -->
</select>
This code checks if attributeValue
exists in form()
and passes propertyLocationID
to the template. The template then selects the appropriate option based on propertyLocationID
.
Modifying getValue()
Enhance the attribute to return a location object. Add this to Application\Attribute\PropertyLocation\Controller
:
use PropCo\Property\Location;
public function getValue()
{
$value = $this->attributeValue->getValueObject();
if ($value) {
return new Location($value->getValue());
}
}
This modification enables getValue()
to return a Location
object instead of just a numeric value.
Important
Avoid recursive loops by using getValueObject()
in the getValue()
method, not getValue()
, because getValue()
checks for a custom getValue()
in the controller.
Update form() Method
Update the form()
method to accommodate the changes:
public function form()
{
if (is_object($this->attributeValue)) {
$number = $this->attributeValue->getValueObject();
if (is_object($number)) {
$this->set('propertyLocationID', $number->getValue());
}
}
}
Add getSearchIndexValue()
To maintain compatibility with the search indexer, implement getSearchIndexValue()
:
public function getSearchIndexValue()
{
if ($this->attributeValue) {
$value = $this->attributeValue->getValueObject();
return $value->getValue();
}
}
This ensures the search indexer receives data in the expected format.
Customizing Attribute Types with Value and Settings Objects
To enhance our attribute, we're adding custom settings and data. This enables creating versatile attribute types.
The "Property Location" attribute lets us select a location for a page. We'll expand it to:
- Allow choosing between a select menu or radio buttons when adding the attribute.
- Enable a custom text label for the location when saving the attribute. This label will be displayed when presenting the attribute.
This requires several updates:
- The attribute controller should no longer inherit from
Concrete\Attribute\Number\Controller
. It needs its own data value objects. - Add a custom label to
PropCo\Property\Location
for usage in our custom object. - Introduce a new settings entity to determine if the attribute key displays as a select list or radio buttons.
- Create a new data value object storing the selected location ID and possibly a custom label.
Concrete CMS's attribute system simplifies adding these elements.
Update the Controller
Change the controller's inheritance for default base attribute controller:
use Concrete\Core\Attribute\Controller as AttributeController;
class Controller extends AttributeController
Add search indexing for an integer in the controller:
protected $searchIndexFieldDefinition = ['type' => 'integer', 'options' => ['default' => 0, 'notnull' => false]];
Update PropCo\Property\Location
Add a custom label, getter, and setter in the constructor:
namespace PropCo\Property;
class Location {
protected $propertyLocationID;
protected $customLabel;
// Constants for location IDs here
public function __construct($propertyLocationID, $customLabel) {
$this->propertyLocationID = $propertyLocationID;
$this->customLabel = $customLabel;
}
// Getter methods for ID and label here
}
Create a Settings Object and Form
Create a type_form.php
template and type_form()
method in the controller for adding/updating attributes:
public function type_form() {
// Form content here
}
In type_form.php
, include a fieldset for form settings:
<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="select">Select Menu</option>
<option value="radio_list">Radio Button List</option>
</select>
</div>
</fieldset>
Create the Settings Entity
Create PropertyLocationSettings
class with Doctrine ORM annotations for database setup. Place this file in application/src/Entity/Attribute/Key/Settings/PropertyLocationSettings
.
namespace Application\Entity\Attribute\Key\Settings;
use Concrete\Core\Entity\Attribute\Key\Settings\Settings;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="atPropertyLocationSettings")
*/
class PropertyLocationSettings extends Settings {
/**
* @ORM\Column(type="string")
*/
protected $formDisplayMethod = false;
// Getter and setter for formDisplayMethod here
}
After adding the file, refresh entities in Dashboard > System and Settings > Environment > Database Entities.
Save the Data
Import PropertyLocationSettings
into the controller and implement methods to manage settings:
use Application\Entity\Attribute\Key\Settings\PropertyLocationSettings;
public function getAttributeKeySettingsClass() {
return PropertyLocationSettings::class;
}
public function saveKey($data) {
// Code to handle saving settings based on POST data
}
With these changes, our attribute system now handles more complex interactions and displays.
Update Type Form with Settings
Update type_form()
:
public function type_form()
{
$settings = $this->getAttributeKeySettings();
/**
* @var $settings PropertyLocationSettings
*/
$this->set('formDisplayMethod', $settings->getFormDisplayMethod());
}
Update type_form.php
:
<fieldset>
<legend><?= t('Location Settings') ?></legend>
<!-- ... existing form elements ... -->
</fieldset>
Update Form Rendering Based on Settings
Modify form()
:
public function form()
{
// ... existing code ...
$settings = $this->getAttributeKeySettings();
/**
* @var $settings PropertyLocationSettings
*/
$this->set('formDisplayMethod', $settings->getFormDisplayMethod());
}
Change form.php
for radio list settings:
<?php if (isset($formDisplayMethod) && $formDisplayMethod == 'radio_list') { ?>
<!-- Radio button list -->
<?php } else { ?>
<!-- Select menu -->
<?php } ?>
Create Data Value Entity
Create PropertyLocationValue
entity:
<?php
namespace Application\Entity\Attribute\Value\Value;
use Concrete\Core\Entity\Attribute\Value\Value\AbstractValue;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="atPropertyLocation")
*/
class PropertyLocationValue extends AbstractValue
{
// ... properties and methods ...
}
Refresh entities in Dashboard.
Update Controller for New Data
Modify form()
method:
public function form()
{
// ... updated code to use PropertyLocationValue ...
}
Add Custom Label to Form
Update form template:
<?php if (isset($formDisplayMethod) && $formDisplayMethod == 'radio_list') { ?>
<!-- Radio buttons -->
<?php } else { ?>
<!-- Select menu -->
<?php } ?>
<!-- Custom label input -->
Update Search Indexer
Modify getSearchIndexValue()
:
public function getSearchIndexValue()
{
if ($this->attributeValue) {
$value = $this->attributeValue->getValueObject();
return $value->getPropertyLocationID();
}
}
Handle Data Saving
Implement required methods:
createAttributeValueFromRequest()
createAttributeValue($mixed)
getAttributeValueClass()
Implementations should manage the attribute data and return PropertyLocationValue
instances.
Including Attribute Type in Package
Check the packaging a theme documentation to understand Concrete CMS package format and the package controller file.
To add a custom attribute type, like 'Property Location', to a package, such as 'Property Manager' with handle property_manager
:
Create package directories:
mkdir packages/property_manager mkdir packages/property_manager/attributes/
Move entities from
application/src/Entity
topackages/property_manager/src/Concrete/Entity
and update their namespaces fromPropCo\Property\Location
toConcrete\Package\PropertyManager\Entity
.After creating your package controller, move your
property_location
attribute type:mv application/blocks/property_location packages/property_manager/attributes/property_location
In your package's
install()
method, use theConcrete\Core\Application\Application
to instantiateConcrete\Core\Attribute\TypeFactory
:public function install() { $pkg = parent::install(); $factory = $this->app->make('Concrete\Core\Attribute\TypeFactory'); $type = $factory->getByHandle('property_location'); if (!is_object($type)) { $type = $factory->add('property_location', 'Property Location', $pkg); } }
To associate this attribute type with pages:
$service = $this->app->make('Concrete\Core\Attribute\Category\CategoryService'); $category = $service->getByHandle('collection')->getController(); $category->associateAttributeKeyType($type);
$service->getByHandle('collection')
returns a Concrete\Core\Entity\Attribute\Category
object. Use getController
on this entity to obtain the Concrete\Core\Attribute\Category\PageCategory
class.
Making Attribute Types Searchable
Attributes can enable custom search functionality for objects. To make an attribute type searchable, ensure a valid search index definition exists in the attribute type controller. For instance, for a 'Property Location' attribute, the search index definition might be:
protected $searchIndexFieldDefinition = array('type' => 'integer', 'options' => array('default' => 0, 'notnull' => false));
This definition stores integer values in the search index. Implement getSearchIndexValue
in the controller to insert the correct data into the index:
public function getSearchIndexValue() {
// Implementation details...
}
For the search interface, create a search.php
file in the attributes/property_location folder with a select menu. Implement the search()
and searchForm()
methods in the controller for frontend interaction and backend searching:
public function search() {
// Implementation details...
}
public function searchForm($list) {
// Implementation details...
}
Searching against Multiple Fields
To search against multiple fields, define multiple index fields in the searchIndexFieldDefinition
array:
protected $searchIndexFieldDefinition = array(
// Multiple field definitions...
);
Adjust the searchForm()
method to handle multiple search fields:
public function searchForm($list) {
// Implementation details...
}
Handling Search via Keyword
To enable keyword search, implement the searchKeywords()
method in the controller:
public function searchKeywords($keywords, $queryBuilder) {
// Implementation details...
}
This method uses the QueryBuilder
object to perform keyword-based searches on the attribute data.