As documented on the official SilverStripe website, you can render DataObjects as individual pages using controller actions. This is fine if you're comfortable in using the ID of the DataObject as part of the page URL, but if you're looking to create and use human-friendly URLs similar to that of SiteTree pages, then you will want to implement something that generates hyphenated and sanitized URL segments instead.
You can consider a "URL segment" to be a sanitized, human-friendly readable string generated from the Title
field of the DataObject it is intended for. It doesn't contain any special URL encoding characters, and strictly only uses alpha-numeric characters. Anything that isn't alpha numeric is replaced by a hyphen.
By default, these are automatically generated for pages that derive from the SiteTree type. Unfortunately this same kind of behaviour does not exist for DataObjects, but this can be easily remedied by introducing a new data extension to your project and applying it to your DataObject type's list of extensions.
<?php
use SilverStripe\ORM\DataExtension;
use SilverStripe\View\Parsers\URLSegmentFilter;
class URLSegmentExtension extends DataExtension
{
private static $db = [
'URLSegment' => 'Varchar(255)'
];
public function onBeforeWrite()
{
parent::onBeforeWrite();
if ($this->owner->hasField('URLSegment')) {
if (!$this->owner->URLSegment) {
$this->owner->URLSegment = $this->generateURLSegment($this->owner->Title);
}
if (!$this->owner->isInDB() || $this->owner->isChanged('URLSegment')) {
$this->owner->URLSegment = $this->generateURLSegment($this->owner->URLSegment);
$this->makeURLSegmentUnique();
}
}
}
public function IsURLSegmentInUse($URLSegment)
{
$class = $this->owner;
$items = $class::get()->filter('URLSegment', $URLSegment);
if ($this->owner->ID > 0) {
$items = $items->exclude('ID', $this->owner->ID);
}
return $items->exists();
}
public function makeURLSegmentUnique()
{
$count = 2;
$currentURLSegment = $this->owner->URLSegment;
while ($this->IsURLSegmentInUse($currentURLSegment)) {
$currentURLSegment = preg_replace('/-[0-9]+$/', '', $currentURLSegment) . '-' . $count;
++$count;
}
$this->owner->URLSegment = $currentURLSegment;
}
public function generateURLSegment($title)
{
$filter = URLSegmentFilter::create();
$filteredTitle = $filter->filter($title);
$ownerClassName = $this->owner->ClassName;
$ownerClassName = strtolower($ownerClassName);
if (!$filteredTitle || $filteredTitle == '-' || $filteredTitle == '-1') {
$filteredTitle = "$ownerClassName-$this->ID";
}
return $filteredTitle;
}
}
Breakdown
There's a bit to digest here, but the summary of what's going on in this extension is the following.
- This extension registers the field "
URLSegment
" to whichever DataObject that this extension type is appended to through the.yml
configuration file. - Each time the DataObject is published or written through from the Object-Relational Mapping system, it will automatically generate a new URL segment string if none has been generated yet.
- It achieves this by checking the
URLSegment
field, and if it has not been changed or updated, then it will attempt to use theTitle
field of the DataObject to generate a string suitable for theURLSegment
database field. - It checks recursively to see if there is an existing DataObject in the database that has the generated
URLSegment
already. - If there is an existing DataObject in the database with the generated
URLSegment
, it will instead increment an integer, append it to the string, and recursively check again to see if the newly generatedURLSegment
has already been assigned. - Once it has determined that the generated string is not in use, it will then apply it to the DataObject's field and continue with committing the data to the database.
Usage
Now that we have an applicable URLSegment
string for our DataObject, we can put it into use in a similar manner as what has already been described on these pages. Except, in this instance we will be making use of the URLSegment
field for filtering (based on the $Action
value) instead of the ID of the DataObject.
The code for achieving this would look something like this.
if (is_numeric($params['ID'])) {
$eventImages = EventImage::get()->filter([
'EventID' => $this->dataRecord->ID,
'ID' => intval($params['ID'])
]);
} else {
$eventImages = EventImage::get()->filter([
'EventID' => $this->dataRecord->ID,
'URLSegment' => $params['ID']
]);
}
Ensure that you have configured your project to use this data extension. In the example configuration file below, I am applying the extension that I've written above to the DataObject that I want the URL segments to be generated for.
Once the configuration file is saved, simply navigate to /dev/build?flush=all
on your project, and this should now automatically generate the URL segments each time you publish modifications to your DataObjects.
---
Name: urlsegments
---
Portfolio\Models\TechnologyTag:
extensions:
- Portfolio\Extensions\URLSegmentExtension
And that's all there is to it. You could make further modifications if you like, including ensuring that the URLSegment
field for a DataObject uses the appropriate form field when modifying it from the CMS admin.
Top comments (0)