WEBcoast Logo

Domain model der Blog-Extension erweitern

Die Domain-Models einer fremden Extension zu erweitern ist Extbase bisher von Haus aus nicht vorgesehen. Zum Glück hat evoWeb die Extension `extender` entwickelt und veröffentlicht, um genau diese Lücke zu schließen, wie sie selbst schreiben.

Ich wollte zwei Sachen umsetzen, die standardmäßig von der Blog-Extension nicht unterstützt wurden:

  1. Die manuelle Sortierung der Kategorien nutzen, wenn diese im Frontend bei einem Post angezeigt werden.
  2. Einzelne Post als "pinned" oder "top post" zu markieren, wie man es von der News-Extension kennt.

`extender` habe ich über composer installiert:

composer req evoweb/extender

Danach habe ich meine beiden Teil-Domain-Models erstellt, eins für Kategorie und eins für 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;
    }
}

Die Felder habe jeweils im TCA konfiguriert. Bei `sorting` hat das einizig und allein den Grund, dass Extbase' Data Mapper das Feld auch auf mein Model mappt.

<?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');

Anschließend habe ich meine beiden Model-Klassen für `extender` registriert. Dabei musste ich `agency_pack` als Extension angeben, weil `AgencyPack` der zweite Teils des originalen Namespace ist und damit als Extension-Key genutzt wird. Nicht optimal, macht aber soweit keine Probleme.

<?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;

Für die Sortierung habe ich mir einen generalisierten Sortierungs-View-Helper gebschrieben, der sowohl array als auch Traversable - und damit Extbase' QueryResultInterface als Typ unterstützt.

<?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;
    }
}

Zum Schluss habe ich den ViewHelper noch als Filter im Template eingebaut.

<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>

Ich hoffe, das ist hilfreich. Teilen und Kopieren sind ausdrücklich erlaubt.