biesior
biesior

Reputation: 55798

TYPO3/Extbase How to create unique slug within create action?

I have slug field in my TCA and in general it works, when adding via Backend > List module, even if I won't input any value the unique eval ensures that slug will be unique, so when I'll create many rows with the same name Foo TYPO3 backend will enshure that it will resolve to unique slugs like foo, foo-1, foo-2, etc. Kudos!:

'slug'       => [
    'exclude'     => true,
    'label'       => 'Slug',
    'displayCond' => 'VERSION:IS:false',
    'config'      => [
        'type'              => 'slug',
        'generatorOptions'  => [
            'fields'         => ['name'],
            'fieldSeparator' => '/',
            'replacements'   => [
                '/' => '',
            ],
        ],
        'fallbackCharacter' => '-',
        'eval'              => 'unique',
        'default'           => '',
        'appearance'        => [
            'prefix' => \BIESIOR\Garage\UserFunctions\SlugPrefix::class . '->getPrefix'
        ],
    ],
],

However when creating a new object from my form within new/create actions (typical Extbase CRUD from extension_builder as you can see) like:

public function createAction(Car $newCar)
{
    $this->addFlashMessage(
        'The object was created. Please be aware that this action is publicly accessible unless you implement an access check. See https://docs.typo3.org/typo3cms/extensions/extension_builder/User/Index.html', 
        '', 
        \TYPO3\CMS\Core\Messaging\AbstractMessage::WARNING);
    $this->carRepository->add($newCar);
    $this->redirect('list');
}

of course slug is note set.

My first idea is to duplicate the logic of TCA type='slug' and just add this functionality with some own JS, AJAX and PHP, however that sounds as overload and time consumption. Especially that I don't want the user to care about slug part at all. Is there any simple API for lookup for a unique slug of the given table that can be used in custom action instead?

Note this question is not about how to handle it with JS, that's just concept. I would like to skip this part for FE user at all, he doesn't need to know what the slug is. Just during creating a new object, I want to get unique value like foo-123 instead.

Upvotes: 1

Views: 4523

Answers (3)

biesior
biesior

Reputation: 55798

According to answers from Elias and Jonas, I created a class which simplifies things especially when you have more models to handle

typo3conf/ext/sitepackage/Classes/Utility/SlugUtility.php

<?php
namespace VENDOR\Sitepackage\Utility; // <- to be replaced with your namespace

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/***
 *
 * This file is part of the "Sitepackage" Extension for TYPO3 CMS.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 *  (c) 2020 Marcus Biesioroff <[email protected]>
 *  Concept by:  Elias Häußler
 *               Jonas Eberle
 *
 ***/
class SlugUtility
{
    /**
     * @param int    $uid UID of record saved in DB
     * @param string $tableName Name of the table to lookup for uniques
     * @param string $slugFieldName Name of the slug field
     *
     * @return string Resolved unique slug
     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
     */
    public static function generateUniqueSlug(int $uid, string $tableName, string $slugFieldName): string
    {

        /** @var Connection $connection */
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
        $queryBuilder = $connection->createQueryBuilder();

        $record = $queryBuilder
            ->select('*')
            ->from($tableName)
            ->where('uid=:uid')
            ->setParameter(':uid', $uid)
            ->execute()
            ->fetch();
        if (!$record) return false;

//      Get field configuration
        $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$slugFieldName]['config'];
        $evalInfo = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);

//      Initialize Slug helper
        /** @var SlugHelper $slugHelper */
        $slugHelper = GeneralUtility::makeInstance(
            SlugHelper::class,
            $tableName,
            $slugFieldName,
            $fieldConfig
        );
//      Generate slug
        $slug = $slugHelper->generate($record, $record['pid']);
        $state = RecordStateFactory::forName($tableName)
            ->fromArray($record, $record['pid'], $record['uid']);

//      Build slug depending on eval configuration
        if (in_array('uniqueInSite', $evalInfo)) {
            $slug = $slugHelper->buildSlugForUniqueInSite($slug, $state);
        } else if (in_array('uniqueInPid', $evalInfo)) {
            $slug = $slugHelper->buildSlugForUniqueInPid($slug, $state);
        } else if (in_array('unique', $evalInfo)) {
            $slug = $slugHelper->buildSlugForUniqueInTable($slug, $state);
        }
        return $slug;
    }
}

Usage in any place, like controller. Scheduler task, repository, etc. Keep in mind that record should be saved before (it may be created by Extbase, or just with plain SQL), just need to have created uid and be valid TYPO3 record.

use VENDOR\Sitepackage\Utility\SlugUtility;
use \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;

...

$pageSlug = SlugUtility::generateUniqueSlug(
    5,        // int     $uid            UID of record saved in DB
    'pages',  // string  $tableName      Name of the table to lookup for uniques
    'slug'    // string  $slugFieldName  Name of the slug field
)

// or

$uniqueSlug = SlugUtility::generateUniqueSlug(
    123,
    'tx_garage_domain_model_car',
    'slug'
);

// or according to the original question, 
// if you created new model object with Extbase, 
// persist it, create unique slug with SlugUtility 
// set the slug property to the created model object and finally update

public function createAction(Car $newCar)
{
    $this->carRepository->add($newCar);
    GeneralUtility::makeInstance(PersistenceManager::class)->persistAll();
    $uniqueSlug = SlugUtility::generateUniqueSlug(
        $newCar->getUid(),
        'tx_garage_domain_model_car',
        'slug'
    );
    if($uniqueSlug) {
        $newCar->setSlug($uniqueSlug);
        $this->carRepository->update($newCar);
    }
    $this->redirect('list');
}

// no need for second call to persistAll() 
// as Extbase will call it at action's finalizing.

// etc.

Upvotes: 5

Jonas Eberle
Jonas Eberle

Reputation: 2921

You can use the SlugHelper directly. The API was obviously not made very fluent for that use case but it works...

$this->carRepository->add($newCar);

// probably you need to persist first - I am not sure if this is really necessary
$this->objectManager()->get(
  \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class
)->persistAll();

$table = 'tx_garage_domain_model_car';
$field = 'slug';

// a stripped down record with just the necessary fields is enough
$record = ['name' => $newCar->getName()];
$pid = $this->settings->persistence->... 

$slugHelper = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
  \TYPO3\CMS\Core\DataHandling\SlugHelper::class,
  $table,
  $field,
  $GLOBALS['TCA'][$table]['columns'][$field]['config']
);

$newCar->slug = $slugHelper->generate($record, $pid);

Upvotes: 2

eliashaeussler
eliashaeussler

Reputation: 206

In addition to Jonas Eberles answer here's another example which also respects the eval configuration of the slug field (can be uniqueInSite, uniqueInPid or simply unique).

use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;

...

public function createAction(Car $newCar)
{
    $this->carRepository->add($newCar);
    GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class)->persistAll();
    $record = $this->carRepository->findByUidAssoc($newCar->getUid())[0];

    $tableName = 'tx_garage_domain_model_car';
    $slugFieldName = 'slug';

//      Get field configuration
    $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$slugFieldName]['config'];
    $evalInfo = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);

//      Initialize Slug helper
    /** @var SlugHelper $slugHelper */
    $slugHelper = GeneralUtility::makeInstance(
        SlugHelper::class,
        $tableName,
        $slugFieldName,
        $fieldConfig
    );

//      Generate slug

    $slug = $slugHelper->generate($record, $record['pid']);
    $state = RecordStateFactory::forName($tableName)
        ->fromArray($record, $record['pid'], $record['uid']);

//      Build slug depending on eval configuration
    if (in_array('uniqueInSite', $evalInfo)) {
        $slug = $slugHelper->buildSlugForUniqueInSite($slug, $state);
    } else if (in_array('uniqueInPid', $evalInfo)) {
        $slug = $slugHelper->buildSlugForUniqueInPid($slug, $state);
    } else if (in_array('unique', $evalInfo)) {
        $slug = $slugHelper->buildSlugForUniqueInTable($slug, $state);
    }
    $newCar->setSlug($slug);
    $this->carRepository->update($newCar);

}

with custom finder in the repository to fetch assoc array instead of the mapped object for $racord argument

public function findByUidAssoc($uid)
{
    $query = $this->createQuery();
    $query->matching(
        $query->equals('uid', $uid)
    );

    return $query->execute(true)[0];
}

Note that the record needs to be persisted before executing above code.

References:

Upvotes: 9

Related Questions