WEBcoast Logo

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:

  1. Using the manual sorting of the category records when displaying the categories of a post.
  2. 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.