Extending the domain model of the blog extension
Extending the domain model of existing extension has always been a pain in the ... you know. Extbase is unfortunately lacking this very importance feature. But... luckily evoWeb created the `extender` extension, which exatly fills this gap, as they say in the manual.
I wanted to achieve two things, which the blog extension did not provide by default:
- Using the manual sorting of the category records when displaying the categories of a post.
- Marking a single post as "pinned" or "top post" as some know it from the very popular news extension.
I installed `extender` through composer by
composer req evoweb/extender
A created two partial models, one for the category and one for the post.
<?php
declare(strict_types=1);
namespace Vendor\Sitepackage\Domain\Model;
class Category
{
protected int $sorting;
public function getSorting(): int
{
return $this->sorting;
}
public function setSorting(int $sorting): void
{
$this->sorting = $sorting;
}
}
<?php
declare(strict_types=1);
namespace Vendor\Sitepackage\Domain\Model;
class Post
{
protected bool $isTopPost;
public function getIsTopPost(): bool
{
return $this->isTopPost;
}
public function setIsTopPost(bool $isTopPost): void
{
$this->isTopPost = $isTopPost;
}
}
I added both fields to the TCA. For `sorting` the sole purpose is to make the extbase data mapper recognize and map the field to my model.
<?php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('sys_category', [
'sorting' => [
'label' => 'Sorting (only for extbase mapping)',
'config' => [
'type' => 'passthrough'
]
]
]);
<?php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('pages', [
'is_top_post' => [
'label' => 'LLL:EXT:sitepackage/Resources/Private/Language/locallang_backend.xlf:pages.is_top_post',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => []
],
],
]);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes('pages', 'is_top_post', \T3G\AgencyPack\Blog\Constants::DOKTYPE_BLOG_POST, 'after:publish_date');
Afterwards I registered both model classes for `extender`. Doing so I needed to use `agency_pack` as the extension because `AgencyPack` ist the second part of the original namespace and thereby used as the extension key. Not perfect, but does not seem to cause any problems.
<?php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['agency_pack']['extender'][\T3G\AgencyPack\Blog\Domain\Model\Category::class]['sitepackage'] = \Vendor\Sitepackage\Domain\Model\Category::class;
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['agency_pack']['extender'][\T3G\AgencyPack\Blog\Domain\Model\Post::class]['sitepackage'] = \Vendor\Sitepackage\Domain\Model\Post::class;
For handling the sorting, I wrote general sort view helper, that could handle array and Traversable types like extbase' QueryResultInterface.
<?php
declare(strict_types=1);
namespace Vender\Sitepackage\ViewHelpers;
use Traversable;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
class SortViewHelper extends AbstractViewHelper
{
public function initializeArguments()
{
$this->registerArgument('collection', 'array|' . Traversable::class, 'The collection to sort');
$this->registerArgument('sortBy', 'string', 'Property/key to sort by', true);
$this->registerArgument('order', 'string', 'Sorting direction: asc or desc', false, 'asc');
}
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
{
$collection = $arguments['collection'] ?? $renderChildrenClosure();
$sortBy = $arguments['sortBy'];
$order = strtolower($arguments['order']);
if ($collection instanceof Traversable) {
$collection = iterator_to_array($collection);
}
usort($collection, function($a, $b) use ($sortBy, $order) {
$sortA = self::getProperty($a, $sortBy);
$sortB = self::getProperty($b, $sortBy);
if ($sortA > $sortB) {
return $order === 'asc' ? 1 : -1;
}
if ($sortA < $sortB) {
return $order === 'asc' ? -1 : 1;
}
return 0;
});
return $collection;
}
protected static function getProperty($data, $property)
{
if (is_array($data)) {
if (array_key_exists($property, $data)) {
return $data[$property];
}
return null;
} else {
$reflectionClass = new \ReflectionClass($data);
$getter = 'get' . ucfirst($property);
$isser = 'is' . ucfirst($property);
$hasser = 'has' . ucfirst($property);
foreach ([$getter, $isser, $hasser] as $method) {
if ($reflectionClass->hasMethod($method) && $reflectionClass->getMethod($method)->isPublic()) {
return $data->$getter();
}
}
if ($reflectionClass->hasProperty($property) && $reflectionClass->getProperty($property)->isPublic()) {
return $data->$property;
}
}
return null;
}
}
Finally I used the view helper in the template as a filter.
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:blog="http://typo3.org/ns/T3G/AgencyPack/Blog/ViewHelpers" xmlns:sp="http://typo3.org/ns/Vendor/Sitepackage/ViewHelpers" data-namespace-typo3-fluid="true">
...
<f:for each="{posts -> sp:sort(sortBy: 'isTopPost', order: 'desc')}" as="post">
<f:render section="Post" arguments="{_all}" />
</f:for>
...
<f:section name="Post">
...
<f:for each="{post.categories -> sp:sort(sortBy: 'sorting')}" as="category">
...
</f:for>
...
</f:section>
...
</html>
I hope this is helpful. Feel free to share, copy and use any of the code examples.