Skip to content

Commit

Permalink
2.2.3
Browse files Browse the repository at this point in the history
[FEATURE] Support TCA type group with more than one allowed table in extbase models (use resolveMmRelationWithDifferentTables in getter).
[FEATURE] New TcaService method (convertTableNameToClassNames) looks up domain model class names connected to a given table.
[FEATURE] Add helper trait to apply an arbitrary number of constraints to a query in a clean way.
[BUGFIX] Original TCA was overwritten by extending classes even for those properties that weren't overridden.
[BUGFIX] convertString converts version numbers to float.
  • Loading branch information
phantasie-schmiede authored Jul 29, 2024
1 parent c457422 commit bcb1b45
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 12 deletions.
46 changes: 40 additions & 6 deletions Classes/Attribute/TCA/ColumnType/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Psr\Container\NotFoundExceptionInterface;
use ReflectionException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use function is_array;

/**
* Class Group
Expand All @@ -29,12 +30,16 @@ class Group extends AbstractColumnType
protected TcaService $tcaService;

/**
* @param string|null $allowed https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Group/Properties/Allowed.html
* @param array|null $elementBrowserEntryPoints https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Group/Properties/ElementBrowserEntryPoints.html
* @param string|null $foreignTable https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Group/Properties/ForeignTable.html
* @param string $linkedModel Instead of directly specifying a foreign table, it is possible to
* specify a domain model class.
* @param int|null $maxItems https://docs.typo3.org/m/typo3/reference-tca/12.4/en-us/ColumnsConfig/CommonProperties/Maxitems.html
* $mmOppositeUsage automatically populates $allowed if it's empty.
*
* @param string|null $allowed https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Group/Properties/Allowed.html
* @param array|null $elementBrowserEntryPoints https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Group/Properties/ElementBrowserEntryPoints.html
* @param string|null $foreignTable https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Group/Properties/ForeignTable.html
* @param string $linkedModel Instead of directly specifying a foreign table, it is
* possible to specify a domain model class.
* @param int|null $maxItems https://docs.typo3.org/m/typo3/reference-tca/12.4/en-us/ColumnsConfig/CommonProperties/Maxitems.html
* @param string|null $mm https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Group/Properties/Mm.html
* @param array|null $mmOppositeUsage https://docs.typo3.org/m/typo3/reference-tca/12.4/en-us/ColumnsConfig/Type/Group/Properties/Mm.html#confval-group-mm-opposite-usage
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
Expand All @@ -46,12 +51,31 @@ public function __construct(
protected ?string $foreignTable = null,
protected string $linkedModel = '',
protected ?int $maxItems = null,
protected ?string $mm = null,
protected ?array $mmOppositeUsage = null,
) {
$this->tcaService = GeneralUtility::makeInstance(TcaService::class);

if (class_exists($linkedModel)) {
$this->foreignTable = $this->tcaService->convertClassNameToTableName($linkedModel);
}

if (!empty($mmOppositeUsage)) {
$this->mmOppositeUsage = [];

foreach ($mmOppositeUsage as $modelOrTableName => $fieldOrPropertyNames) {
$this->mmOppositeUsage[$this->tcaService->convertClassNameToTableName($modelOrTableName)] = array_map(
fn(string $fieldOrPropertyName) => $this->tcaService->convertPropertyNameToColumnName(
$fieldOrPropertyName
),
$fieldOrPropertyNames
);
}

if (null === $this->allowed) {
$this->allowed = implode(',', array_keys($this->mmOppositeUsage));
}
}
}

public function getAllowed(): ?string
Expand Down Expand Up @@ -82,4 +106,14 @@ public function getMaxItems(): ?int
{
return $this->maxItems;
}

public function getMm(): ?string
{
return $this->mm;
}

public function getMmOppositeUsage(): ?array
{
return $this->mmOppositeUsage;
}
}
61 changes: 58 additions & 3 deletions Classes/Service/Configuration/TcaService.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PSB\PsbFoundation\Exceptions\ImplementationException;
use PSB\PsbFoundation\Exceptions\MisconfiguredTcaException;
use PSB\PsbFoundation\Service\ExtensionInformationService;
use PSB\PsbFoundation\Utility\ArrayUtility;
use PSB\PsbFoundation\Utility\Configuration\TcaUtility;
use PSB\PsbFoundation\Utility\LocalizationUtility;
use PSB\PsbFoundation\Utility\ReflectionUtility;
Expand Down Expand Up @@ -151,9 +152,7 @@ public function addToPalette(string $identifier, array $fieldNames, string $posi
*/
public function buildTca(bool $overrideMode): void
{
if (false === self::$allowCaching || empty(self::$classTableMapping)) {
$this->buildClassesTableMapping();
}
$this->getClassesTableMapping();

if ($overrideMode) {
$key = self::CLASS_TABLE_MAPPING_KEYS['TCA_OVERRIDES'];
Expand Down Expand Up @@ -244,6 +243,35 @@ public function convertPropertyNameToColumnName(string $propertyName, string $cl
return GeneralUtility::camelCaseToLowerCaseUnderscored($propertyName);
}

/**
* This uses the internal mapping of class names to table names to do a reverse lookup.
* The result is an array of class names which are mapped to the given table name.
* If no class is found, an empty array is returned.
*
* @throws ContainerExceptionInterface
* @throws ImplementationException
* @throws NotFoundExceptionInterface
* @throws ReflectionException
*/
public function convertTableNameToClassNames(string $tableName): array
{
$searchResults = ArrayUtility::inArrayRecursive(
$this->getClassesTableMapping(),
$tableName
);

if (!empty($searchResults)) {
// Explode each result path and keep only last part.
return array_map(static function($item) {
$arrayPathParts = explode('.', $item);

return array_pop($arrayPathParts);
}, $searchResults);
}

return [];
}

/**
* An existing palette with given identifier would be overwritten!
*
Expand Down Expand Up @@ -286,6 +314,21 @@ public function createPalette(string $identifier, string $label = '', string $de
$GLOBALS['TCA'][$this->tableName]['palettes'][$identifier] = $paletteConfiguration;
}

/**
* @throws ContainerExceptionInterface
* @throws ImplementationException
* @throws NotFoundExceptionInterface
* @throws ReflectionException
*/
public function getClassesTableMapping(): array
{
if (false === self::$allowCaching || empty(self::$classTableMapping)) {
$this->buildClassesTableMapping();
}

return self::$classTableMapping;
}

/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
Expand Down Expand Up @@ -400,6 +443,18 @@ protected function buildFromAttributes(string $className, bool $overrideMode): v
}

$properties = $reflection->getProperties();

if ($overrideMode) {
/*
* Filter out properties of parent class that are not overridden in current class to keep original
* configuration!
*/
$properties = array_filter($properties, static function($property) use ($reflection) {
return $property->getDeclaringClass()
->getName() === $reflection->getName();
});
}

$columnConfigurations = [];

foreach ($properties as $property) {
Expand Down
85 changes: 84 additions & 1 deletion Classes/Service/ObjectService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@
namespace PSB\PsbFoundation\Service;

use Exception;
use PSB\PsbFoundation\Attribute\TCA\ColumnType\Mm;
use PSB\PsbFoundation\Attribute\TCA\ColumnType\Select;
use PSB\PsbFoundation\Exceptions\ImplementationException;
use PSB\PsbFoundation\Service\Configuration\TcaService;
use PSB\PsbFoundation\Utility\ObjectUtility;
use PSB\PsbFoundation\Utility\ReflectionUtility;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Repository;
use function get_class;

/**
Expand All @@ -29,9 +39,82 @@ class ObjectService
{
public function __construct(
protected readonly ConnectionPool $connectionPool,
protected readonly TcaService $tcaService,
) {
}

/**
* This can be used to get a collection of domain models from a mm-relation of type group with more than one table
* allowed.
* If the related table name can be resolved to a domain model, the result will include an instance of it,
* otherwise a database row will be fetched (no overlays are applied yet!. If more than one domain model is mapped
* to a single table, the preferred models (array of full qualified class names) will be used as filter. The first
* mapping result is used if there is no preferred model.
*
* @throws ContainerExceptionInterface
* @throws ImplementationException
* @throws NotFoundExceptionInterface
* @throws ReflectionException
*/
public function resolveMmRelationWithDifferentTables(
AbstractEntity $domainModel,
string $property,
array $preferredModels = [],
): array {
$columnName = $this->tcaService->convertPropertyNameToColumnName($property, $domainModel::class);
$tableName = $this->tcaService->convertClassNameToTableName($domainModel::class);
$fieldConfiguration = $GLOBALS['TCA'][$tableName]['columns'][$columnName]['config'];

if ('group' !== $fieldConfiguration['type']) {
throw new RuntimeException(
__CLASS__ . ': The property "' . $property . '" of object "' . get_class(
$domainModel
) . '" is not of TCA type group!', 1721396926
);
}

$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$relationHandler->start(
$domainModel->_getProperty($property),
$fieldConfiguration['allowed'] ?? $fieldConfiguration['foreign_table'] ?? '',
$fieldConfiguration['MM'] ?? '',
$domainModel->getUid(),
$tableName,
$fieldConfiguration
);

$relationHandler->processDeletePlaceholder();

$result = [];
$repositories = [];

foreach ($relationHandler->itemArray as $item) {
$classNames = $this->tcaService->convertTableNameToClassNames($item['table']);

if (!empty($classNames)) {
$classNames = (array_intersect($classNames, $preferredModels) ?: $classNames);
$className = array_shift($classNames);
$repositoryClassName = ObjectUtility::getRepositoryClassName($className);

if (isset($repositories[$repositoryClassName])) {
$repository = $repositories[$repositoryClassName];
} elseif (class_exists($repositoryClassName)) {
$repository = GeneralUtility::makeInstance($repositoryClassName);
$repositories[$repositoryClassName] = $repository;
}

if (isset($repository) && $repository instanceof Repository) {
$result[] = $repository->findByUid($item['id']);
continue;
}
}

$result[] = BackendUtility::getRecord($item['table'], $item['id']);
}

return $result;
}

/**
* If you have a select field in TCA with 'multiple' set to true, Extbase still returns each selected record only
* once. This method returns the whole selected set sorted as in backend.
Expand Down
1 change: 1 addition & 0 deletions Classes/Utility/Configuration/TcaUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class TcaUtility
'mmInsertFields' => 'MM_insert_fields',
'mmMatchFields' => 'MM_match_fields',
'mmOppositeField' => 'MM_opposite_field',
'mmOppositeUsage' => 'MM_oppositeUsage',
'selIconField' => 'selicon_field',
'sortBy' => 'sortby',
'typeIconClasses' => 'typeicon_classes',
Expand Down
12 changes: 12 additions & 0 deletions Classes/Utility/ObjectUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use ReflectionException;
use ReflectionMethod;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

/**
* Class ObjectUtility
Expand Down Expand Up @@ -50,6 +51,17 @@ public static function getFullQualifiedClassName(string $className, array $names
return false;
}

public static function getRepositoryClassName(AbstractEntity|string $modelInstanceOrModelClassName): string
{
if ($modelInstanceOrModelClassName instanceof AbstractEntity) {
$modelClassName = $modelInstanceOrModelClassName::class;
} else {
$modelClassName = $modelInstanceOrModelClassName;
}

return str_replace('\Model\\', '\Repository\\', $modelClassName) . 'Repository';
}

/**
* @throws ReflectionException
*/
Expand Down
14 changes: 14 additions & 0 deletions Classes/Utility/QueryUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use function count;

/**
* Class QueryUtility
Expand All @@ -24,6 +25,19 @@
*/
class QueryUtility
{
public static function applyConstraints(array $constraints, QueryInterface $query): void
{
switch (count($constraints)) {
case 0:
break;
case 1:
$query->matching($constraints[0]);
break;
default:
$query->matching($query->logicalAnd(...$constraints));
}
}

/**
* @throws Exception
*/
Expand Down
10 changes: 9 additions & 1 deletion Classes/Utility/StringUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ public static function convertString(
return $variable;
}

/*
* Try string-to-number conversion in these cases:
* - single zero
* - float beginning with "0," or "0."
* - float not ending with zero (see line 85)
* - any other number not beginning with a zero
*/
if (1 === strlen($variable) || !str_starts_with($variable, '0') || in_array(
$variable[1],
[
Expand All @@ -74,7 +81,8 @@ public static function convertString(

$floatRepresentation = filter_var(str_replace(',', '.', $variable), FILTER_VALIDATE_FLOAT);

if (false !== $floatRepresentation) {
// Avoid string manipulation by "rounding away" trailing zeros!
if (false !== $floatRepresentation && !str_ends_with($variable, '0')) {
return $floatRepresentation;
}
}
Expand Down
4 changes: 4 additions & 0 deletions Tests/Unit/Utility/StringUtilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public static function convertStringDataProvider(): Generator
'0123',
'0123',
];
yield 'floats with trailing zeros are not truncated (e.g. version numbers)' => [
'2024.10',
'2024.10',
];
yield 'boolean false' => [
'false',
false,
Expand Down
2 changes: 1 addition & 1 deletion ext_emconf.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
'description' => 'Configuration framework for TYPO3 extension development',
'state' => 'stable',
'title' => 'PSbits | Foundation',
'version' => '2.2.1',
'version' => '2.2.3',
];

0 comments on commit bcb1b45

Please sign in to comment.