TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
|
||||
|
||||
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
|
||||
|
||||
/**
|
||||
* A column map to map a column configured in $TCA on a property of a domain object.
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
final readonly class ColumnMap
|
||||
{
|
||||
/**
|
||||
* @param string $columnName Name of the DB column
|
||||
* @param TableColumnType $type TCA column type like "input", "inline"
|
||||
* @param string|null $dateTimeFormat DataTime format (TCA "format" property). Allowed: "date", "datetime", "time", "timesec", "datetimesec"
|
||||
* @param string|null $dateTimeStorageFormat Alternative DataTime format instead of using unix timestamps (TCA "dbType" property). Allowed: "date", "datetime", "time"
|
||||
* @param Relation|null $typeOfRelation Extbase "Relation" enum if any
|
||||
* @param string|null $childTableName TCA "foreign_table" if any, @todo: Does not consider group "allowed" for multi table relations
|
||||
* @param string|null $relationTableName TCA "MM" if any
|
||||
* @param array $relationTableMatchFields TCA "MM_match_fields" if any in MM, TCA "foreign_match_fields" if any
|
||||
* @param string|null $parentKeyFieldName TCA "uid_local" or "uid_foreign" with TCA "MM" depending on "opposite",
|
||||
* TCA "foreign_field" with TCA "foreign_table" relations
|
||||
* @param string|null $parentTableFieldName TCA "foreign_table_field" with TCA "foreign_table" relations
|
||||
* @param string|null $childKeyFieldName TCA "uid_local" or "uid_foreign" with TCA "MM" depending on "opposite"
|
||||
* @param string|null $childSortByFieldName Name of the field results from child's table are sorted by:
|
||||
* TCA "sorting" or "sorting_foreign" with TCA "MM" depending on TCA "opposite" situation,
|
||||
* TCA "foreign_sortby" with TCA "foreign_table"
|
||||
* @param string|null $childTableDefaultSortings name of the fields with direction results from child's table are sorted by default:
|
||||
* TCA "foreign_default_sortby" with TCA "foreign_table"
|
||||
*/
|
||||
public function __construct(
|
||||
public string $columnName,
|
||||
public TableColumnType $type,
|
||||
public ?string $dateTimeFormat = null,
|
||||
public ?string $dateTimeStorageFormat = null,
|
||||
public ?Relation $typeOfRelation = Relation::NONE,
|
||||
public ?string $childTableName = null,
|
||||
public ?string $relationTableName = null,
|
||||
public array $relationTableMatchFields = [],
|
||||
public ?string $parentKeyFieldName = null,
|
||||
public ?string $parentTableFieldName = null,
|
||||
public ?string $childKeyFieldName = null,
|
||||
public ?string $childSortByFieldName = null,
|
||||
public ?string $childTableDefaultSortings = null,
|
||||
public bool $isNullable = false,
|
||||
) {}
|
||||
|
||||
// Getters below could be removed but don't harm much and kept as b/w compat for now.
|
||||
|
||||
public function getTypeOfRelation(): Relation
|
||||
{
|
||||
return $this->typeOfRelation;
|
||||
}
|
||||
|
||||
public function getColumnName(): string
|
||||
{
|
||||
return $this->columnName;
|
||||
}
|
||||
|
||||
public function getChildTableName(): ?string
|
||||
{
|
||||
return $this->childTableName;
|
||||
}
|
||||
|
||||
public function getChildTableDefaultSortings(): ?string
|
||||
{
|
||||
return $this->childTableDefaultSortings;
|
||||
}
|
||||
|
||||
public function getChildSortByFieldName(): ?string
|
||||
{
|
||||
return $this->childSortByFieldName;
|
||||
}
|
||||
|
||||
public function getRelationTableName(): ?string
|
||||
{
|
||||
return $this->relationTableName;
|
||||
}
|
||||
|
||||
public function getRelationTableMatchFields(): array
|
||||
{
|
||||
return $this->relationTableMatchFields;
|
||||
}
|
||||
|
||||
public function getParentKeyFieldName(): ?string
|
||||
{
|
||||
return $this->parentKeyFieldName;
|
||||
}
|
||||
|
||||
public function getParentTableFieldName(): ?string
|
||||
{
|
||||
return $this->parentTableFieldName;
|
||||
}
|
||||
|
||||
public function getChildKeyFieldName(): ?string
|
||||
{
|
||||
return $this->childKeyFieldName;
|
||||
}
|
||||
|
||||
public function getDateTimeFormat(): ?string
|
||||
{
|
||||
return $this->dateTimeFormat;
|
||||
}
|
||||
|
||||
public function getDateTimeStorageFormat(): ?string
|
||||
{
|
||||
return $this->dateTimeStorageFormat;
|
||||
}
|
||||
|
||||
public function getType(): TableColumnType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isNullable(): bool
|
||||
{
|
||||
return $this->isNullable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
enum Relation
|
||||
{
|
||||
case NONE;
|
||||
case HAS_ONE;
|
||||
case HAS_MANY;
|
||||
case BELONGS_TO_MANY;
|
||||
case HAS_AND_BELONGS_TO_MANY;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
|
||||
|
||||
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
||||
use TYPO3\CMS\Core\Schema\Field\CountryFieldType;
|
||||
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
|
||||
use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
|
||||
use TYPO3\CMS\Core\Schema\Field\FolderFieldType;
|
||||
use TYPO3\CMS\Core\Schema\Field\RelationalFieldTypeInterface;
|
||||
use TYPO3\CMS\Core\Schema\Field\StaticSelectFieldType;
|
||||
use TYPO3\CMS\Core\Schema\RelationshipType;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
|
||||
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
readonly class ColumnMapFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ReflectionService $reflectionService,
|
||||
) {}
|
||||
|
||||
public function create(FieldTypeInterface $field, string $propertyName, string $className): ColumnMap
|
||||
{
|
||||
$propertyType = null;
|
||||
$propertyCollectionValueType = null;
|
||||
try {
|
||||
$property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
|
||||
$nonProxyPropertyTypes = $property->getFilteredTypes([$property, 'filterLazyLoadingProxyAndLazyObjectStorage']);
|
||||
$primaryType = $nonProxyPropertyTypes[0] ?? null;
|
||||
$propertyType = $primaryType?->getClassName() ?? $primaryType?->getBuiltinType() ?? null;
|
||||
if ($primaryType?->isCollection() && $primaryType->getCollectionValueTypes() !== []) {
|
||||
$primaryCollectionValueType = $primaryType->getCollectionValueTypes()[0];
|
||||
$propertyCollectionValueType = $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType();
|
||||
}
|
||||
} catch (NoSuchPropertyException) {
|
||||
// $type and $propertyCollectionValueType kept null
|
||||
}
|
||||
|
||||
// @todo: The relation related handling below smells fishy at various places. Some TCA
|
||||
// details are ignored, some are at least opinionated, some are wrong. The combination
|
||||
// of fetching details from TCA *and* the model class makes everything quite complex.
|
||||
// This should be consolidated.
|
||||
// Also, the mixture of extbase internal "Relation", core TableColumnType, plus
|
||||
// core TcaSchema details is complex and should be simplified to what we really need.
|
||||
// Last, ColumnMap is not fully used throughout extbase, various details tend to
|
||||
// still access TCA details directly.
|
||||
// In the end, we may be better off removing extbase "Relation" altogether and
|
||||
// add TcaSchema $columnConfiguration to ColumnMap to sort out TCA details at
|
||||
// the few places where needed directly? This would be more in-line with DataHandler
|
||||
// as well and raises fewer state questions in consumers, which reduces complexity.
|
||||
|
||||
$columnConfiguration = $field->getConfiguration();
|
||||
$columnName = $field->getName();
|
||||
$tableColumnType = TableColumnType::tryFrom($field->getType());
|
||||
$childTableName = null;
|
||||
if ($field->isType(TableColumnType::GROUP)) {
|
||||
// TCA type="group" has no TCA property "foreign_table" and can only deal with single-table
|
||||
// relations in extbase (no support for union types). That means `allowed` should only
|
||||
// contain ONE table entry, as Extbase can only evaluate the first one, if multiple
|
||||
// are defined.
|
||||
$allowed = GeneralUtility::trimExplode(',', $columnConfiguration['allowed'] ?? '', true);
|
||||
$childTableName = $allowed[0] ?? $columnConfiguration['foreign_table'] ?? null;
|
||||
} elseif ($field instanceof RelationalFieldTypeInterface) {
|
||||
$childTableName = $columnConfiguration['foreign_table'] ?? null;
|
||||
}
|
||||
|
||||
if ($field instanceof DateTimeFieldType) {
|
||||
// TCA type="datetime" considers "dbtype" and is done.
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
dateTimeFormat: $field->getFormat(),
|
||||
dateTimeStorageFormat: $field->getPersistenceType(),
|
||||
isNullable: $field->isNullable(),
|
||||
);
|
||||
}
|
||||
|
||||
if (($field instanceof RelationalFieldTypeInterface) && $field->getRelationshipType() === RelationshipType::ManyToMany) {
|
||||
if (!isset($columnConfiguration['MM'])) {
|
||||
throw new \LogicException(
|
||||
'TCA schema of column ' . $columnName . ' is "ManytoMany", but TCA config has no MM property set',
|
||||
1733560101
|
||||
);
|
||||
}
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
typeOfRelation: Relation::HAS_AND_BELONGS_TO_MANY,
|
||||
childTableName: $childTableName,
|
||||
relationTableName: $columnConfiguration['MM'],
|
||||
relationTableMatchFields: is_array($columnConfiguration['MM_match_fields'] ?? false) ? $columnConfiguration['MM_match_fields'] : [],
|
||||
parentKeyFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'uid_foreign' : 'uid_local',
|
||||
childKeyFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'uid_local' : 'uid_foreign',
|
||||
childSortByFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'sorting_foreign' : 'sorting',
|
||||
isNullable: $field->isNullable(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($propertyCollectionValueType !== null) {
|
||||
// The field might not be a RelationFieldType, e.g. for TCA type "passthrough" or type "select"
|
||||
// without items. However, the model defines a relation and therefore overrules the TCA schema lookup.
|
||||
// This also overrules any "maxitems" or "renderType" configuration!
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
typeOfRelation: Relation::HAS_MANY,
|
||||
childTableName: $childTableName,
|
||||
relationTableMatchFields: is_array($columnConfiguration['foreign_match_fields'] ?? false) ? $columnConfiguration['foreign_match_fields'] : [],
|
||||
parentKeyFieldName: $columnConfiguration['foreign_field'] ?? null,
|
||||
parentTableFieldName: $columnConfiguration['foreign_table_field'] ?? null,
|
||||
childSortByFieldName: $columnConfiguration['foreign_sortby'] ?? null,
|
||||
childTableDefaultSortings: $columnConfiguration['foreign_default_sortby'] ?? null,
|
||||
isNullable: $field->isNullable(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($propertyType !== null && strpbrk($propertyType, '_\\') !== false) {
|
||||
// @todo: Check this. Seems to be a check for Tx_Foo_Bar style class names?!
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
typeOfRelation: Relation::HAS_ONE,
|
||||
childTableName: $childTableName,
|
||||
relationTableMatchFields: is_array($columnConfiguration['foreign_match_fields'] ?? false) ? $columnConfiguration['foreign_match_fields'] : [],
|
||||
parentKeyFieldName: $columnConfiguration['foreign_field'] ?? null,
|
||||
parentTableFieldName: $columnConfiguration['foreign_table_field'] ?? null,
|
||||
childSortByFieldName: $columnConfiguration['foreign_sortby'] ?? null,
|
||||
isNullable: $field->isNullable(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($field instanceof FolderFieldType) {
|
||||
// Folder is a special case which always has a relation to one or many "folders".
|
||||
// In case "maxitems" is set to > 1 and relationship is not explicitly set to "*toOne"
|
||||
// it's HAS_MANY, in all other cases it's HAS_ONE. It can never belong to many.
|
||||
// @todo: Get rid of the "maxitems" and rely purely on the evaluated relationship type
|
||||
// @todo: TCA type="folder" has no TCA property "relationship"!
|
||||
$relation = Relation::HAS_ONE;
|
||||
if (!in_array((string)($columnConfiguration['relationship'] ?? ''), ['oneToOne', 'manyToOne'], true)
|
||||
&& (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1)
|
||||
) {
|
||||
$relation = Relation::HAS_MANY;
|
||||
}
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
typeOfRelation: $relation,
|
||||
isNullable: $field->isNullable(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($field instanceof CountryFieldType) {
|
||||
$relation = Relation::HAS_ONE;
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
typeOfRelation: $relation,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
$field instanceof RelationalFieldTypeInterface
|
||||
&& $field->getRelationshipType()->hasMany()
|
||||
&& (
|
||||
!$field->isType(TableColumnType::GROUP, TableColumnType::SELECT)
|
||||
|| ($field->isType(TableColumnType::GROUP) && (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1))
|
||||
|| ($field->isType(TableColumnType::SELECT) && (($columnConfiguration['renderType'] ?? '') !== 'selectSingle' || (int)($columnConfiguration['maxitems'] ?? 0) > 1))
|
||||
)
|
||||
)
|
||||
|| (
|
||||
$field instanceof StaticSelectFieldType
|
||||
&& (int)($columnConfiguration['maxitems'] ?? 0) > 1 // @todo: Get rid of the "maxitems" and rely purely on the relationship type
|
||||
)
|
||||
) {
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
typeOfRelation: Relation::HAS_MANY,
|
||||
isNullable: $field->isNullable(),
|
||||
);
|
||||
}
|
||||
|
||||
return new ColumnMap(
|
||||
columnName: $columnName,
|
||||
type: $tableColumnType,
|
||||
isNullable: $field->isNullable(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
|
||||
|
||||
/**
|
||||
* A data map to map a single table configured in $TCA on a domain object.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
final readonly class DataMap
|
||||
{
|
||||
/**
|
||||
* @param string $className Name of the class this column map represents
|
||||
* @param string $tableName Name of the DB table this column map is located on
|
||||
* @param string|null $recordType The record type stored in the "type" field as configured in $TCA
|
||||
* @param array $subclasses List of subclasses of the current class
|
||||
* @param array<non-empty-string, ColumnMap> $columnMaps List of TCA columns with their ColumnMap representation
|
||||
* @param string|null $languageIdColumnName Name of a column holding the language id of the record, often "sys_language_uid"
|
||||
* @param string|null $translationOriginColumnName Name of a column holding the uid of the record this record is a translation of, often "l10n_parent" or "l18n_parent"
|
||||
* @param string|null $translationOriginDiffSourceName Name of a column holding the diff data for the record this record is a translation of, often "l10n_diffsource" or "l10n_diffsource"
|
||||
* @param string|null $modificationDateColumnName Name of a column holding the timestamp the record was last modified, often "tstamp"
|
||||
* @param string|null $creationDateColumnName Name of a column holding the creation date timestamp, often "crdate"
|
||||
* @param string|null $deletedFlagColumnName Name of a column indicating the soft deleted state of the row, often "deleted"
|
||||
* @param string|null $disabledFlagColumnName Name of a column indicating the "hidden in frontend" state of the row, often "hidden" or "disabled"
|
||||
* @param string|null $startTimeColumnName Name of a column holding the timestamp the record should not be displayed before, often "starttime"
|
||||
* @param string|null $endTimeColumnName Name of a column holding the timestamp the record should not be displayed afterward, often "endtime"
|
||||
* @param string|null $frontendUserGroupColumnName Name of a column holding the uid of the front-end user group which is allowed to edit this record
|
||||
* @param string|null $recordTypeColumnName Name of a column holding the record type, example: "CType" in table "tt_content"
|
||||
* @param bool $rootLevel Bool cast of TCA[$tableName]['ctrl']['rootLevel']
|
||||
*/
|
||||
public function __construct(
|
||||
public string $className,
|
||||
public string $tableName,
|
||||
public ?string $recordType = null,
|
||||
public array $subclasses = [],
|
||||
public array $columnMaps = [],
|
||||
public ?string $languageIdColumnName = null,
|
||||
public ?string $translationOriginColumnName = null,
|
||||
public ?string $translationOriginDiffSourceName = null,
|
||||
public ?string $modificationDateColumnName = null,
|
||||
public ?string $creationDateColumnName = null,
|
||||
public ?string $deletedFlagColumnName = null,
|
||||
public ?string $disabledFlagColumnName = null,
|
||||
public ?string $startTimeColumnName = null,
|
||||
public ?string $endTimeColumnName = null,
|
||||
public ?string $frontendUserGroupColumnName = null,
|
||||
public ?string $recordTypeColumnName = null,
|
||||
public bool $rootLevel = false,
|
||||
) {}
|
||||
|
||||
public function getColumnMap(string $propertyName): ?ColumnMap
|
||||
{
|
||||
return $this->columnMaps[$propertyName] ?? null;
|
||||
}
|
||||
|
||||
public function isPersistableProperty(string $propertyName): bool
|
||||
{
|
||||
return isset($this->columnMaps[$propertyName]);
|
||||
}
|
||||
|
||||
// Getters below could be removed but don't harm much and kept as b/w compat for now.
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return $this->className;
|
||||
}
|
||||
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getRecordType(): ?string
|
||||
{
|
||||
return $this->recordType;
|
||||
}
|
||||
|
||||
public function getSubclasses(): array
|
||||
{
|
||||
return $this->subclasses;
|
||||
}
|
||||
|
||||
public function getLanguageIdColumnName(): ?string
|
||||
{
|
||||
return $this->languageIdColumnName;
|
||||
}
|
||||
|
||||
public function getTranslationOriginColumnName(): ?string
|
||||
{
|
||||
return $this->translationOriginColumnName;
|
||||
}
|
||||
|
||||
public function getTranslationOriginDiffSourceName(): ?string
|
||||
{
|
||||
return $this->translationOriginDiffSourceName;
|
||||
}
|
||||
|
||||
public function getModificationDateColumnName(): ?string
|
||||
{
|
||||
return $this->modificationDateColumnName;
|
||||
}
|
||||
|
||||
public function getCreationDateColumnName(): ?string
|
||||
{
|
||||
return $this->creationDateColumnName;
|
||||
}
|
||||
|
||||
public function getDeletedFlagColumnName(): ?string
|
||||
{
|
||||
return $this->deletedFlagColumnName;
|
||||
}
|
||||
|
||||
public function getDisabledFlagColumnName(): ?string
|
||||
{
|
||||
return $this->disabledFlagColumnName;
|
||||
}
|
||||
|
||||
public function getStartTimeColumnName(): ?string
|
||||
{
|
||||
return $this->startTimeColumnName;
|
||||
}
|
||||
|
||||
public function getEndTimeColumnName(): ?string
|
||||
{
|
||||
return $this->endTimeColumnName;
|
||||
}
|
||||
|
||||
public function getFrontEndUserGroupColumnName(): ?string
|
||||
{
|
||||
return $this->frontendUserGroupColumnName;
|
||||
}
|
||||
|
||||
public function getRecordTypeColumnName(): ?string
|
||||
{
|
||||
return $this->recordTypeColumnName;
|
||||
}
|
||||
|
||||
public function getRootLevel(): bool
|
||||
{
|
||||
return $this->rootLevel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Persistence\ClassesConfiguration;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException;
|
||||
|
||||
/**
|
||||
* A factory for a data map to map a single table configured in $TCA on a domain object.
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
readonly class DataMapFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ClassesConfiguration $classesConfiguration,
|
||||
private ColumnMapFactory $columnMapFactory,
|
||||
private TcaSchemaFactory $tcaSchemaFactory,
|
||||
#[Autowire(expression: 'service("package-dependent-cache-identifier").toString()')]
|
||||
private string $baseCacheIdentifier,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
private FrontendInterface $firstLevelCache,
|
||||
#[Autowire(service: 'cache.extbase')]
|
||||
private FrontendInterface $secondLevelCache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Builds a data map by adding column maps for all the configured columns in the $TCA.
|
||||
* It also resolves the type of values the column is holding and the typo of relation the column
|
||||
* represents.
|
||||
*
|
||||
* @param string $className The class name you want to fetch the Data Map for
|
||||
*/
|
||||
public function buildDataMap(string $className): DataMap
|
||||
{
|
||||
$className = ltrim($className, '\\');
|
||||
$cacheIdentifierClassName = str_replace('\\', '', $className) . '_';
|
||||
$cacheIdentifier = 'DataMap_' . $cacheIdentifierClassName . $this->baseCacheIdentifier;
|
||||
$dataMap = $this->firstLevelCache->get($cacheIdentifier);
|
||||
if ($dataMap instanceof DataMap) {
|
||||
return $dataMap;
|
||||
}
|
||||
$dataMap = $this->secondLevelCache->get($cacheIdentifier);
|
||||
if ($dataMap instanceof DataMap) {
|
||||
$this->firstLevelCache->set($cacheIdentifier, $dataMap);
|
||||
return $dataMap;
|
||||
}
|
||||
$dataMap = $this->buildDataMapInternal($className);
|
||||
$this->firstLevelCache->set($cacheIdentifier, $dataMap);
|
||||
$this->secondLevelCache->set($cacheIdentifier, $dataMap);
|
||||
return $dataMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a data map by adding column maps for all the configured columns in the $TCA.
|
||||
* It also resolves the type of values the column is holding and the typo of relation the column
|
||||
* represents.
|
||||
*
|
||||
* @param string $className The class name you want to fetch the Data Map for
|
||||
* @throws InvalidClassException
|
||||
*/
|
||||
protected function buildDataMapInternal(string $className): DataMap
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
throw new InvalidClassException(
|
||||
'Could not find class definition for name "' . $className . '". This could be caused by a mis-spelling of the class name in the class definition.',
|
||||
1476045117
|
||||
);
|
||||
}
|
||||
|
||||
$recordType = null;
|
||||
$subclasses = [];
|
||||
$tableName = $this->resolveTableName($className);
|
||||
$fieldNameToPropertyNameMapping = [];
|
||||
if ($this->classesConfiguration->hasClass($className)) {
|
||||
$classSettings = $this->classesConfiguration->getConfigurationFor($className);
|
||||
$subclasses = $this->classesConfiguration->getSubClasses($className);
|
||||
if (isset($classSettings['recordType']) && $classSettings['recordType'] !== '') {
|
||||
$recordType = (string)$classSettings['recordType'];
|
||||
}
|
||||
if (isset($classSettings['tableName']) && $classSettings['tableName'] !== '') {
|
||||
$tableName = $classSettings['tableName'];
|
||||
}
|
||||
foreach ($classSettings['properties'] ?? [] as $propertyName => $propertyDefinition) {
|
||||
$fieldNameToPropertyNameMapping[$propertyDefinition['fieldName']] = $propertyName;
|
||||
}
|
||||
}
|
||||
|
||||
$schema = null;
|
||||
$languageCapability = null;
|
||||
$columnMaps = [];
|
||||
if ($this->tcaSchemaFactory->has($tableName)) {
|
||||
$schema = $this->tcaSchemaFactory->get($tableName);
|
||||
if ($schema->hasCapability(TcaSchemaCapability::Language)) {
|
||||
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
||||
}
|
||||
foreach ($schema->getFields() as $columnName => $columnDefinition) {
|
||||
$propertyName = $fieldNameToPropertyNameMapping[$columnName] ?? GeneralUtility::underscoredToLowerCamelCase($columnName);
|
||||
$columnMaps[$propertyName] = $this->columnMapFactory->create($columnDefinition, $propertyName, $className);
|
||||
}
|
||||
}
|
||||
|
||||
return new DataMap(
|
||||
className: $className,
|
||||
tableName: $tableName,
|
||||
recordType: $recordType,
|
||||
subclasses: $subclasses,
|
||||
columnMaps: $columnMaps,
|
||||
languageIdColumnName: $languageCapability?->getLanguageField()->getName(),
|
||||
translationOriginColumnName: $languageCapability?->getTranslationOriginPointerField()->getName(),
|
||||
translationOriginDiffSourceName: $languageCapability?->hasDiffSourceField()
|
||||
? $languageCapability->getDiffSourceField()->getName()
|
||||
: null,
|
||||
modificationDateColumnName: $schema?->hasCapability(TcaSchemaCapability::UpdatedAt)
|
||||
? (string)$schema->getCapability(TcaSchemaCapability::UpdatedAt)
|
||||
: null,
|
||||
creationDateColumnName: $schema?->hasCapability(TcaSchemaCapability::CreatedAt)
|
||||
? (string)$schema->getCapability(TcaSchemaCapability::CreatedAt)
|
||||
: null,
|
||||
deletedFlagColumnName: $schema?->hasCapability(TcaSchemaCapability::SoftDelete)
|
||||
? (string)$schema->getCapability(TcaSchemaCapability::SoftDelete)
|
||||
: null,
|
||||
disabledFlagColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
|
||||
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)
|
||||
: null,
|
||||
startTimeColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionStartTime)
|
||||
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionStartTime)
|
||||
: null,
|
||||
endTimeColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionEndTime)
|
||||
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionEndTime)
|
||||
: null,
|
||||
frontendUserGroupColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionUserGroup)
|
||||
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)
|
||||
: null,
|
||||
// @todo Check how to resolve foreign table types properly - if possible at all in this scenario
|
||||
recordTypeColumnName: $schema?->supportsSubSchema() && !$schema->getSubSchemaTypeInformation()->isPointerToForeignFieldInForeignSchema()
|
||||
? $schema->getSubSchemaTypeInformation()->getFieldName()
|
||||
: null,
|
||||
// @todo We should remove DataMap in order to use TcaSchema directly
|
||||
rootLevel: (bool)($schema?->getCapability(TcaSchemaCapability::RestrictionRootLevel)->getRootLevelType()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the table name for the given class name
|
||||
*/
|
||||
protected function resolveTableName(string $className): string
|
||||
{
|
||||
$className = ltrim($className, '\\');
|
||||
$classNameParts = explode('\\', $className);
|
||||
// Skip vendor and product name for core classes
|
||||
if (str_starts_with($className, 'TYPO3\\CMS\\')) {
|
||||
$classPartsToSkip = 2;
|
||||
} else {
|
||||
$classPartsToSkip = 1;
|
||||
}
|
||||
return 'tx_' . strtolower(implode('_', array_slice($classNameParts, $classPartsToSkip)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,977 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
|
||||
|
||||
use Doctrine\Instantiator\InstantiatorInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
use TYPO3\CMS\Core\Country\Country;
|
||||
use TYPO3\CMS\Core\Country\CountryProvider;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
||||
use TYPO3\CMS\Core\Database\RelationHandler;
|
||||
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFactory;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
|
||||
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
|
||||
use TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent;
|
||||
use TYPO3\CMS\Extbase\Persistence;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LoadingStrategyInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\NonExistentPropertyException;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\UnknownPropertyTypeException;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Session;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
|
||||
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoPropertyTypesException;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
|
||||
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
|
||||
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
|
||||
|
||||
/**
|
||||
* A mapper to map database tables configured in $TCA on domain objects.
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true, shared: false)]
|
||||
class DataMapper
|
||||
{
|
||||
/**
|
||||
* @var QueryInterface|null
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
public function __construct(
|
||||
private readonly ReflectionService $reflectionService,
|
||||
private readonly QueryObjectModelFactory $qomFactory,
|
||||
private readonly Session $persistenceSession,
|
||||
private readonly DataMapFactory $dataMapFactory,
|
||||
private readonly QueryFactoryInterface $queryFactory,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly InstantiatorInterface $instantiator,
|
||||
private readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
private readonly CountryProvider $countryProvider,
|
||||
) {}
|
||||
|
||||
public function setQuery(QueryInterface $query): void
|
||||
{
|
||||
$this->query = $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the given rows on objects
|
||||
*
|
||||
* @param string $className The name of the class
|
||||
* @param array $rows An array of arrays with field_name => value pairs
|
||||
* @return array An array of objects of the given class
|
||||
* @template T of DomainObjectInterface
|
||||
* @phpstan-param class-string<T> $className
|
||||
* @phpstan-return list<T>
|
||||
*/
|
||||
public function map($className, array $rows)
|
||||
{
|
||||
$objects = [];
|
||||
foreach ($rows as $row) {
|
||||
$objects[] = $this->mapSingleRow($this->getTargetType($className, $row), $row);
|
||||
}
|
||||
return $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target type for the given row.
|
||||
*
|
||||
* @param string $className The name of the class
|
||||
* @param array $row A single array with field_name => value pairs
|
||||
* @return string The target type (a class name)
|
||||
* @phpstan-param class-string $className
|
||||
* @phpstan-return class-string
|
||||
*/
|
||||
public function getTargetType($className, array $row)
|
||||
{
|
||||
$dataMap = $this->getDataMap($className);
|
||||
$targetType = $className;
|
||||
if ($dataMap->recordTypeColumnName !== null) {
|
||||
foreach ($dataMap->subclasses as $subclassName) {
|
||||
$recordSubtype = $this->getDataMap($subclassName)->recordType;
|
||||
if ((string)$row[$dataMap->recordTypeColumnName] === (string)$recordSubtype) {
|
||||
$targetType = $subclassName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $targetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a single row on an object of the given class
|
||||
*
|
||||
* @param string $className The name of the target class
|
||||
* @param array $row A single array with field_name => value pairs
|
||||
* @return object An object of the given class
|
||||
* @template T of DomainObjectInterface
|
||||
* @phpstan-param class-string<T> $className
|
||||
* @phpstan-return T
|
||||
*/
|
||||
protected function mapSingleRow($className, array $row)
|
||||
{
|
||||
$identifier = $this->buildIdentifier($row);
|
||||
if ($this->persistenceSession->hasIdentifier($identifier, $className)) {
|
||||
$object = $this->persistenceSession->getObjectByIdentifier($identifier, $className);
|
||||
} else {
|
||||
$object = $this->createEmptyObject($className);
|
||||
$this->persistenceSession->registerObject($object, $identifier);
|
||||
$this->thawProperties($object, $row);
|
||||
$event = new AfterObjectThawedEvent($object, $row);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$object->_memorizeCleanState();
|
||||
$this->persistenceSession->registerReconstitutedEntity($object);
|
||||
}
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a language-aware identifier for the identity map.
|
||||
*
|
||||
* The identifier includes the UID, localized UID (if present), and the
|
||||
* language content identifier to ensure objects loaded with different
|
||||
* language configurations are cached separately.
|
||||
*
|
||||
* @param array $row A single array with field_name => value pairs
|
||||
* @return non-empty-string The identifier for the identity map
|
||||
*/
|
||||
protected function buildIdentifier(array $row): string
|
||||
{
|
||||
return $this->persistenceSession->buildIdentifier($row, $this->getEffectiveLanguageAspect());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective LanguageAspect for the current mapping context.
|
||||
*
|
||||
* Returns the LanguageAspect from the current query if available,
|
||||
* otherwise returns a default LanguageAspect for default language.
|
||||
*/
|
||||
protected function getEffectiveLanguageAspect(): LanguageAspect
|
||||
{
|
||||
return $this->query?->getQuerySettings()->getLanguageAspect() ?? new LanguageAspect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a skeleton of the specified object. This is
|
||||
* designed to *not* call class constructor when hydrating,
|
||||
* but *do call* initializeObject() if exists and obey
|
||||
* eventually registered implementation overrides ("xclass").
|
||||
*
|
||||
* @param class-string $className Name of the class to create a skeleton for
|
||||
* @throws InvalidClassException
|
||||
* @template T of DomainObjectInterface
|
||||
* @phpstan-param class-string<T> $className
|
||||
* @phpstan-return T
|
||||
*/
|
||||
protected function createEmptyObject(string $className): DomainObjectInterface
|
||||
{
|
||||
// Note: The class_implements() function also invokes autoload to assure that the interfaces
|
||||
// and the class are loaded. Would end up with __PHP_Incomplete_Class without it.
|
||||
if (!in_array(DomainObjectInterface::class, class_implements($className) ?: [])) {
|
||||
throw new InvalidClassException('Cannot create empty instance of the class "' . $className
|
||||
. '" because it does not implement the TYPO3\\CMS\\Extbase\\DomainObject\\DomainObjectInterface.', 1234386924);
|
||||
}
|
||||
// Use GU::getClassName() to obey class implementation overrides.
|
||||
$object = $this->instantiator->instantiate(GeneralUtility::getClassName($className));
|
||||
if (is_callable($callable = [$object, 'initializeObject'])) {
|
||||
$callable();
|
||||
}
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given properties on the object.
|
||||
*
|
||||
* @param DomainObjectInterface $object The object to set properties on
|
||||
* @throws NonExistentPropertyException
|
||||
* @throws UnknownPropertyTypeException
|
||||
*/
|
||||
protected function thawProperties(DomainObjectInterface $object, array $row)
|
||||
{
|
||||
$className = get_class($object);
|
||||
$classSchema = $this->reflectionService->getClassSchema($className);
|
||||
$dataMap = $this->getDataMap($className);
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_UID, (int)$row['uid']);
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_PID, (int)($row['pid'] ?? 0));
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID, (int)$row['uid']);
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID, (int)$row['uid']);
|
||||
if ($dataMap->languageIdColumnName !== null) {
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID, (int)($row[$dataMap->languageIdColumnName] ?? 0));
|
||||
if (isset($row['_LOCALIZED_UID'])) {
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID, (int)$row['_LOCALIZED_UID']);
|
||||
}
|
||||
}
|
||||
if (!empty($row['_ORIG_uid']) && $this->tcaSchemaFactory->get($dataMap->tableName)->isWorkspaceAware()) {
|
||||
$object->_setProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID, (int)$row['_ORIG_uid']);
|
||||
}
|
||||
foreach ($classSchema->getDomainObjectProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
if (!$dataMap->isPersistableProperty($propertyName)) {
|
||||
continue;
|
||||
}
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
if ($columnMap === null) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($row[$columnMap->columnName])) {
|
||||
continue;
|
||||
}
|
||||
$propertyValue = $row[$columnMap->columnName];
|
||||
|
||||
$nonProxyPropertyTypes = $property->getFilteredTypes([$property, 'filterLazyLoadingProxyAndLazyObjectStorage']);
|
||||
if ($nonProxyPropertyTypes === []) {
|
||||
throw new UnknownPropertyTypeException(
|
||||
'The type of property ' . $className . '::' . $propertyName . ' could not be identified, therefore the desired value ('
|
||||
. var_export($propertyValue, true) . ') cannot be mapped onto it. The type of a class property is usually defined via property types or php doc blocks. '
|
||||
. 'Make sure the property has a property type or valid @var tag set which defines the type.',
|
||||
1579965021
|
||||
);
|
||||
}
|
||||
|
||||
if (count($nonProxyPropertyTypes) > 1) {
|
||||
throw new UnknownPropertyTypeException(
|
||||
'The type of property ' . $className . '::' . $propertyName . ' could not be identified because the property is defined as union or intersection type, therefore the desired value ('
|
||||
. var_export($propertyValue, true) . ') cannot be mapped onto it. Make sure to use only a single type.',
|
||||
1660215701
|
||||
);
|
||||
}
|
||||
|
||||
$primaryType = $nonProxyPropertyTypes[0];
|
||||
|
||||
$propertyType = $primaryType->getBuiltinType();
|
||||
$propertyClassName = $primaryType->getClassName();
|
||||
|
||||
$propertyValue = match ($propertyType) {
|
||||
'int', 'integer' => (int)$propertyValue,
|
||||
'bool', 'boolean' => (bool)$propertyValue,
|
||||
'float' => (float)$propertyValue,
|
||||
'string' => (string)$propertyValue,
|
||||
'array' => null, // $this->mapArray($propertyValue); // Not supported, yet!
|
||||
'object' => $this->thawObjectProperty($property, $columnMap, $object, $propertyName, $propertyValue, $propertyClassName),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($propertyValue !== null || $property->isNullable()) {
|
||||
$object->_setProperty($propertyName, $propertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
* @param class-string|null $targetClassName
|
||||
*/
|
||||
private function thawObjectProperty(
|
||||
Property $propertySchema,
|
||||
ColumnMap $columnMap,
|
||||
DomainObjectInterface $parent,
|
||||
string $propertyName,
|
||||
mixed $propertyValue,
|
||||
?string $targetClassName
|
||||
): ?object {
|
||||
if ($targetClassName === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_subclass_of($targetClassName, \BackedEnum::class)) {
|
||||
return $propertySchema->isNullable()
|
||||
? $targetClassName::tryFrom($propertyValue)
|
||||
: $targetClassName::from($propertyValue);
|
||||
}
|
||||
|
||||
if (in_array($targetClassName, [\SplObjectStorage::class, ObjectStorage::class], true)) {
|
||||
return $this->mapResultToPropertyValue(
|
||||
$parent,
|
||||
$propertyName,
|
||||
$this->fetchRelated($parent, $propertyName, $propertyValue)
|
||||
);
|
||||
}
|
||||
|
||||
if (is_subclass_of($targetClassName, \DateTimeInterface::class)) {
|
||||
return $this->mapDateTime(
|
||||
$propertyValue,
|
||||
$columnMap->dateTimeFormat,
|
||||
$columnMap->dateTimeStorageFormat,
|
||||
$columnMap->isNullable,
|
||||
$targetClassName
|
||||
);
|
||||
}
|
||||
|
||||
if ($targetClassName === Country::class || is_subclass_of($targetClassName, Country::class)) {
|
||||
// @todo Check if this can be abstracted in a better way (for future TCA types)
|
||||
// @todo does alpha2 need to be configurable? All storage currently seems to depend on alpha2 in TCA FormEngine
|
||||
return $this->countryProvider->getByAlpha2IsoCode($propertyValue);
|
||||
}
|
||||
|
||||
if (TypeHandlingUtility::isCoreType($targetClassName)) {
|
||||
return $this->mapCoreType($targetClassName, $propertyValue);
|
||||
}
|
||||
|
||||
return $this->mapObjectToClassProperty(
|
||||
$parent,
|
||||
$propertyName,
|
||||
$propertyValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map value to a core type
|
||||
*
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
* @return \TYPO3\CMS\Core\Type\TypeInterface
|
||||
*/
|
||||
protected function mapCoreType($type, $value)
|
||||
{
|
||||
return new $type($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DateTime from a unix timestamp or date/datetime/time value.
|
||||
* If the input is empty, NULL is returned.
|
||||
*
|
||||
* @param int|string $value Unix timestamp or date/datetime/datetimesec value or seconds for time/timesec
|
||||
* @param string|null $format Output format (date/datetime/time/timesec/datetimesec)
|
||||
* @param string|null $storageFormat Storage format for native date/datetime/time/datetimesec fields
|
||||
* @param string $targetType The object class name to be created
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
protected function mapDateTime(
|
||||
$value,
|
||||
$format = null,
|
||||
$storageFormat = null,
|
||||
$isNullable = true,
|
||||
$targetType = \DateTime::class
|
||||
) {
|
||||
$dateTime = DateTimeFactory::createFromDatabaseValueAndTCAConfig(
|
||||
$value,
|
||||
// Reconstruct TCA from our ColumnMap
|
||||
[
|
||||
'type' => 'datetime',
|
||||
'format' => $format,
|
||||
'dbType' => $storageFormat,
|
||||
'nullable' => $isNullable,
|
||||
]
|
||||
);
|
||||
|
||||
return $dateTime === null ? null : match ($targetType) {
|
||||
\DateTimeImmutable::class => $dateTime,
|
||||
\DateTime::class => \DateTime::createFromImmutable($dateTime),
|
||||
default => GeneralUtility::makeInstance($targetType, $dateTime->format('Y-m-d H:i:s.v e')),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a collection of objects related to a property of a parent object
|
||||
*
|
||||
* @param DomainObjectInterface $parentObject The object instance this proxy is part of
|
||||
* @param string $propertyName The name of the proxied property in it's parent
|
||||
* @param mixed $fieldValue The raw field value.
|
||||
* @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded
|
||||
* @return \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage|Persistence\QueryResultInterface The result
|
||||
*/
|
||||
public function fetchRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = true)
|
||||
{
|
||||
$property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
|
||||
if ($enableLazyLoading && $property->isLazy()) {
|
||||
if ($property->isObjectStorageType()) {
|
||||
$result = GeneralUtility::makeInstance(LazyObjectStorage::class, $parentObject, $propertyName, $fieldValue, $this);
|
||||
} elseif (empty($fieldValue)) {
|
||||
$result = null;
|
||||
} else {
|
||||
$result = GeneralUtility::makeInstance(LazyLoadingProxy::class, $parentObject, $propertyName, $fieldValue, $this);
|
||||
}
|
||||
} else {
|
||||
$result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the related objects from the storage backend.
|
||||
*
|
||||
* @param DomainObjectInterface $parentObject The object instance this proxy is part of
|
||||
* @param string $propertyName The name of the proxied property in it's parent
|
||||
* @param mixed $fieldValue The raw field value.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fetchRelatedEager(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
|
||||
{
|
||||
return $fieldValue === '' ? $this->getEmptyRelationValue($parentObject, $propertyName) : $this->getNonEmptyRelationValue($parentObject, $propertyName, $fieldValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $propertyName
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName)
|
||||
{
|
||||
$columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
|
||||
$relatesToOne = $columnMap->typeOfRelation == Relation::HAS_ONE;
|
||||
return $relatesToOne ? null : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $propertyName
|
||||
* @param string $fieldValue
|
||||
* @return Persistence\QueryResultInterface
|
||||
*/
|
||||
protected function getNonEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName, $fieldValue)
|
||||
{
|
||||
$query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the prepared query, ready to be executed.
|
||||
*
|
||||
* @param string $propertyName
|
||||
* @param string $fieldValue
|
||||
* @return Persistence\QueryInterface
|
||||
*/
|
||||
protected function getPreparedQuery(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
|
||||
{
|
||||
$dataMap = $this->getDataMap(get_class($parentObject));
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
$type = $this->getType(get_class($parentObject), $propertyName);
|
||||
$query = $this->queryFactory->create($type);
|
||||
if ($this->query && $query instanceof Query) {
|
||||
$query->setParentQuery($this->query);
|
||||
}
|
||||
$query->getQuerySettings()->setRespectStoragePage(false);
|
||||
$query->getQuerySettings()->setRespectSysLanguage(false);
|
||||
|
||||
$languageAspect = $query->getQuerySettings()->getLanguageAspect();
|
||||
$languageUid = $languageAspect->getContentId();
|
||||
if ($this->query) {
|
||||
$languageAspect = $this->query->getQuerySettings()->getLanguageAspect();
|
||||
$languageUid = $languageAspect->getContentId();
|
||||
if ($dataMap->languageIdColumnName !== null && !$this->query->getQuerySettings()->getRespectSysLanguage()) {
|
||||
//pass language of parent record to child objects, so they can be overlaid correctly in case
|
||||
//e.g. findByUid is used.
|
||||
//the languageUid is used for getRecordOverlay later on, despite RespectSysLanguage being false
|
||||
$parentLanguageUid = (int)$parentObject->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID);
|
||||
// do not override the language when the parent language uid is set to all languages (-1)
|
||||
if ($parentLanguageUid !== -1) {
|
||||
$languageUid = $parentLanguageUid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we always want to overlay relations as most of the time they are stored in db using default language uids
|
||||
$languageAspect = new LanguageAspect(
|
||||
$languageUid,
|
||||
$languageUid,
|
||||
$languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_MIXED : $languageAspect->getOverlayType(),
|
||||
$languageAspect->getFallbackChain()
|
||||
);
|
||||
$query->getQuerySettings()->setLanguageAspect($languageAspect);
|
||||
|
||||
if ($columnMap->typeOfRelation === Relation::HAS_MANY) {
|
||||
if (null !== $orderings = $this->getOrderingsForColumnMap($columnMap)) {
|
||||
$query->setOrderings($orderings);
|
||||
}
|
||||
} elseif ($columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
|
||||
$query->setSource($this->getSource($parentObject, $propertyName));
|
||||
if ($columnMap->childSortByFieldName !== null) {
|
||||
$query->setOrderings([$columnMap->childSortByFieldName => QueryInterface::ORDER_ASCENDING]);
|
||||
}
|
||||
}
|
||||
$query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, $columnMap->relationTableMatchFields));
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orderings array for extbase query by columnMap
|
||||
*
|
||||
* @phpstan-return array<non-empty-string, QueryInterface::ORDER_*>|null
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
public function getOrderingsForColumnMap(ColumnMap $columnMap): ?array
|
||||
{
|
||||
if ($columnMap->childSortByFieldName !== null) {
|
||||
return [$columnMap->childSortByFieldName => QueryInterface::ORDER_ASCENDING];
|
||||
}
|
||||
|
||||
if ($columnMap->childTableDefaultSortings === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$orderings = [];
|
||||
$fields = QueryHelper::parseOrderBy($columnMap->childTableDefaultSortings);
|
||||
foreach ($fields as $field) {
|
||||
$fieldName = $field[0] ?? null;
|
||||
if ($fieldName === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($fieldOrdering = $field[1] ?? null) === null) {
|
||||
$orderings[$fieldName] = QueryInterface::ORDER_ASCENDING;
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldOrdering = strtoupper($fieldOrdering);
|
||||
if (!in_array($fieldOrdering, [QueryInterface::ORDER_ASCENDING, QueryInterface::ORDER_DESCENDING], true)) {
|
||||
$orderings[$fieldName] = QueryInterface::ORDER_ASCENDING;
|
||||
continue;
|
||||
}
|
||||
|
||||
$orderings[$fieldName] = $fieldOrdering;
|
||||
}
|
||||
return $orderings !== [] ? $orderings : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the constraint for multi value properties.
|
||||
*
|
||||
* @param string $propertyName
|
||||
* @param string $fieldValue
|
||||
* @param array $relationTableMatchFields
|
||||
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint
|
||||
*/
|
||||
protected function getConstraint(QueryInterface $query, DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = [])
|
||||
{
|
||||
$dataMap = $this->getDataMap(get_class($parentObject));
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
$workspaceId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id');
|
||||
$parentId = $this->resolveParentId($parentObject, $workspaceId, $columnMap);
|
||||
if ($columnMap && $workspaceId > 0) {
|
||||
$resolvedRelationIds = $this->resolveRelationValuesOfField($dataMap, $columnMap, $parentId, $fieldValue, $workspaceId);
|
||||
} else {
|
||||
$resolvedRelationIds = [];
|
||||
}
|
||||
// Work with the UIDs directly in a workspace
|
||||
if (!empty($resolvedRelationIds)) {
|
||||
$source = $query->getSource();
|
||||
if ($source instanceof JoinInterface) {
|
||||
$constraint = $query->in($source->getJoinCondition()->getProperty1Name(), $resolvedRelationIds);
|
||||
// When querying MM relations directly, Typo3DbQueryParser uses enableFields and thus, filters
|
||||
// out versioned records by default. However, we directly query versioned UIDs here, so we want
|
||||
// to include the versioned records explicitly.
|
||||
if ($columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
|
||||
$query->getQuerySettings()->setEnableFieldsToBeIgnored(['pid']);
|
||||
$query->getQuerySettings()->setIgnoreEnableFields(true);
|
||||
}
|
||||
// Also, we still need to restrict the MM on the foreign side
|
||||
if ($columnMap->getParentKeyFieldName() !== null) {
|
||||
$constraint = $query->logicalAnd(
|
||||
$constraint,
|
||||
$query->equals($columnMap->getParentKeyFieldName(), $parentId)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$constraint = $query->in('uid', $resolvedRelationIds);
|
||||
}
|
||||
if ($columnMap->parentTableFieldName !== null) {
|
||||
$constraint = $query->logicalAnd(
|
||||
$constraint,
|
||||
$query->equals($columnMap->parentTableFieldName, $dataMap->tableName)
|
||||
);
|
||||
}
|
||||
} elseif ($columnMap->parentKeyFieldName !== null) {
|
||||
$value = $parentObject;
|
||||
// If this a MM relation, and MM relations do not know about workspaces, the MM relations always point to the
|
||||
// versioned record, so this must be taken into account here and the versioned record's UID must be used.
|
||||
if ($columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
|
||||
// The versioned UID is used ideally the version ID of a translated record, so this takes precedence over the localized UID
|
||||
if ($value->_hasProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) && $value->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) > 0 && $value->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) !== $value->getUid()) {
|
||||
$value = (int)$value->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID);
|
||||
}
|
||||
}
|
||||
$constraint = $query->equals($columnMap->parentKeyFieldName, $value);
|
||||
if ($columnMap->parentTableFieldName !== null) {
|
||||
$constraint = $query->logicalAnd(
|
||||
$constraint,
|
||||
$query->equals($columnMap->parentTableFieldName, $dataMap->tableName)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Note: $fieldValue is annotated as a string, but this cannot be trusted as the callers do not ensure this.
|
||||
$constraint = $query->in('uid', GeneralUtility::intExplode(',', (string)$fieldValue));
|
||||
}
|
||||
if (!empty($relationTableMatchFields)) {
|
||||
foreach ($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) {
|
||||
$constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue));
|
||||
}
|
||||
}
|
||||
return $constraint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the actual "uid" which we need to query to fetch relations to this UID.
|
||||
*/
|
||||
protected function resolveParentId(DomainObjectInterface $parentObject, int $workspaceId, ?ColumnMap $columnMap): ?int
|
||||
{
|
||||
$parentId = $parentObject->getUid();
|
||||
if ($columnMap && $workspaceId > 0) {
|
||||
// versionedUid in a multi-language setup is the overlaid versioned AND translated ID
|
||||
if ($parentObject->_hasProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) && $parentObject->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) > 0 && $parentObject->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) !== $parentId) {
|
||||
$parentId = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID);
|
||||
} elseif ($parentObject->_hasProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID) && $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID) > 0) {
|
||||
$parentId = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID);
|
||||
}
|
||||
}
|
||||
return $parentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* This resolves relations via RelationHandler and returns their UIDs respectively, and works for MM/ForeignField/CSV in IRRE + Select + Group.
|
||||
*
|
||||
* Note: This only happens for resolving properties for models. When limiting a parentQuery, the Typo3DbQueryParser is taking care of it.
|
||||
*
|
||||
* By using the RelationHandler, the localized, deleted and moved records turn out to be properly resolved
|
||||
* without having to build intermediate queries.
|
||||
*
|
||||
* This is currently only used in workspaces' context, as it is 1 additional DB query needed.
|
||||
*
|
||||
* @param DataMap $dataMap
|
||||
* @param ColumnMap $columnMap
|
||||
* @param int|null $parentId
|
||||
* @param string $fieldValue
|
||||
* @param int $workspaceId
|
||||
* @return array|false|mixed
|
||||
*/
|
||||
protected function resolveRelationValuesOfField(DataMap $dataMap, ColumnMap $columnMap, ?int $parentId, $fieldValue, int $workspaceId)
|
||||
{
|
||||
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
|
||||
$relationHandler->setWorkspaceId($workspaceId);
|
||||
$relationHandler->setUseLiveReferenceIds(true);
|
||||
$relationHandler->setUseLiveParentIds(true);
|
||||
$tableName = $dataMap->tableName;
|
||||
$fieldName = $columnMap->columnName;
|
||||
if (!$this->tcaSchemaFactory->get($tableName)->hasField($fieldName)) {
|
||||
return [];
|
||||
}
|
||||
$fieldConfiguration = $this->tcaSchemaFactory->get($tableName)->getField($fieldName)->getConfiguration();
|
||||
$relationHandler->start(
|
||||
$fieldValue,
|
||||
$fieldConfiguration['allowed'] ?? $fieldConfiguration['foreign_table'] ?? '',
|
||||
$fieldConfiguration['MM'] ?? '',
|
||||
$parentId,
|
||||
$tableName,
|
||||
$fieldConfiguration
|
||||
);
|
||||
$relationHandler->processDeletePlaceholder();
|
||||
$relatedUids = [];
|
||||
if (!empty($relationHandler->tableArray)) {
|
||||
$relatedUids = reset($relationHandler->tableArray);
|
||||
}
|
||||
return $relatedUids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the source to build a join for a m:n relation.
|
||||
*
|
||||
* @param string $propertyName
|
||||
*/
|
||||
protected function getSource(DomainObjectInterface $parentObject, $propertyName): SourceInterface
|
||||
{
|
||||
$columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
|
||||
$left = $this->qomFactory->selector(null, $columnMap->relationTableName);
|
||||
$childClassName = $this->getType(get_class($parentObject), $propertyName);
|
||||
$right = $this->qomFactory->selector($childClassName, $columnMap->childTableName);
|
||||
$joinCondition = $this->qomFactory->equiJoinCondition($columnMap->relationTableName, $columnMap->childKeyFieldName, $columnMap->childTableName, 'uid');
|
||||
return $this->qomFactory->join($left, $right, Query::JCR_JOIN_TYPE_INNER, $joinCondition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mapped classProperty from the identityMap or
|
||||
* mapResultToPropertyValue()
|
||||
*
|
||||
* If the field value is empty and the column map has no parent key field name,
|
||||
* the relation will be empty. If the persistence session has a registered object of
|
||||
* the correct type and identity (fieldValue), this function returns that object.
|
||||
* Otherwise, it proceeds with mapResultToPropertyValue().
|
||||
*
|
||||
* @param mixed $fieldValue the raw field value
|
||||
* @see mapResultToPropertyValue()
|
||||
*/
|
||||
protected function mapObjectToClassProperty(DomainObjectInterface $parentObject, string $propertyName, $fieldValue)
|
||||
{
|
||||
if ($this->propertyMapsByForeignKey($parentObject, $propertyName)) {
|
||||
$result = $this->fetchRelated($parentObject, $propertyName, $fieldValue);
|
||||
return $this->mapResultToPropertyValue($parentObject, $propertyName, $result);
|
||||
}
|
||||
|
||||
if (empty($fieldValue)) {
|
||||
return $this->getEmptyRelationValue($parentObject, $propertyName);
|
||||
}
|
||||
|
||||
$primaryType = $this->reflectionService
|
||||
->getClassSchema(get_class($parentObject))
|
||||
->getProperty($propertyName)
|
||||
->getPrimaryType();
|
||||
|
||||
if ($primaryType === null) {
|
||||
throw NoPropertyTypesException::create($parentObject::class, $propertyName);
|
||||
}
|
||||
|
||||
$className = $primaryType->getClassName();
|
||||
if ($className === null) {
|
||||
throw new \LogicException(
|
||||
sprintf('Evaluated type of class property %s::%s is not a class name. Check the type declaration of the property to use a valid class name.', $parentObject::class, $propertyName),
|
||||
1660217846
|
||||
);
|
||||
}
|
||||
|
||||
$identifier = $this->persistenceSession->buildIdentifier((string)$fieldValue, $this->getEffectiveLanguageAspect());
|
||||
if ($this->persistenceSession->hasIdentifier($identifier, $className)) {
|
||||
return $this->persistenceSession->getObjectByIdentifier($identifier, $className);
|
||||
}
|
||||
|
||||
$result = $this->fetchRelated($parentObject, $propertyName, $fieldValue);
|
||||
return $this->mapResultToPropertyValue($parentObject, $propertyName, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the relation is based on a foreign key.
|
||||
*
|
||||
* @param string $propertyName
|
||||
* @return bool TRUE if the property is mapped
|
||||
*/
|
||||
protected function propertyMapsByForeignKey(DomainObjectInterface $parentObject, $propertyName)
|
||||
{
|
||||
$columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
|
||||
return $columnMap->parentKeyFieldName !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given result as property value of the specified property type.
|
||||
*
|
||||
* @param string $propertyName
|
||||
* @param mixed $result The result
|
||||
* @return mixed
|
||||
*/
|
||||
public function mapResultToPropertyValue(DomainObjectInterface $parentObject, $propertyName, $result)
|
||||
{
|
||||
$propertyValue = null;
|
||||
if ($result instanceof LoadingStrategyInterface) {
|
||||
$propertyValue = $result;
|
||||
} else {
|
||||
$property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
|
||||
$primaryType = $property->getPrimaryType();
|
||||
|
||||
if ($primaryType === null) {
|
||||
throw NoPropertyTypesException::create($parentObject::class, $propertyName);
|
||||
}
|
||||
|
||||
if ($primaryType->getBuiltinType() === 'array' || in_array($primaryType->getClassName(), [\ArrayObject::class, \SplObjectStorage::class, ObjectStorage::class], true)) {
|
||||
$objects = [];
|
||||
foreach ($result as $value) {
|
||||
$objects[] = $value;
|
||||
}
|
||||
if ($primaryType->getClassName() === \ArrayObject::class) {
|
||||
$propertyValue = new \ArrayObject($objects);
|
||||
} elseif ($primaryType->getClassName() === ObjectStorage::class) {
|
||||
$propertyValue = new ObjectStorage();
|
||||
foreach ($objects as $object) {
|
||||
$propertyValue->attach($object);
|
||||
}
|
||||
$propertyValue->_memorizeCleanState();
|
||||
} else {
|
||||
$propertyValue = $objects;
|
||||
}
|
||||
} elseif (strpbrk((string)$primaryType->getClassName(), '_\\') !== false) {
|
||||
// @todo: check the strpbrk function call. Seems to be a check for Tx_Foo_Bar style class names
|
||||
if ($result instanceof QueryResultInterface) {
|
||||
$propertyValue = $result->getFirst();
|
||||
} else {
|
||||
$propertyValue = $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $propertyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of related objects assigned to a property of a parent object
|
||||
*
|
||||
* @param DomainObjectInterface $parentObject The object instance this proxy is part of
|
||||
* @param string $propertyName The name of the proxied property in it's parent
|
||||
* @param mixed $fieldValue The raw field value.
|
||||
* @return int
|
||||
*/
|
||||
public function countRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
|
||||
{
|
||||
$query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
|
||||
return $query->execute()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a data map for a given class name
|
||||
*
|
||||
* @param string $className The class name you want to fetch the Data Map for
|
||||
* @throws Persistence\Generic\Exception
|
||||
*/
|
||||
public function getDataMap($className): DataMap
|
||||
{
|
||||
if (!is_string($className) || $className === '') {
|
||||
throw new Exception('No class name was given to retrieve the Data Map for.', 1251315965);
|
||||
}
|
||||
return $this->dataMapFactory->buildDataMap($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selector (table) name for a given class name.
|
||||
*
|
||||
* @param string $className
|
||||
* @return string The selector name
|
||||
*/
|
||||
public function convertClassNameToTableName($className)
|
||||
{
|
||||
return $this->getDataMap($className)->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the column name for a given property name of the specified class.
|
||||
*
|
||||
* @param string $propertyName
|
||||
* @param string $className
|
||||
* @return string The column name
|
||||
*/
|
||||
public function convertPropertyNameToColumnName($propertyName, $className = null)
|
||||
{
|
||||
if (!empty($className)) {
|
||||
$dataMap = $this->getDataMap($className);
|
||||
$columnMap = $dataMap->getColumnMap($propertyName);
|
||||
if ($columnMap !== null) {
|
||||
return $columnMap->columnName;
|
||||
}
|
||||
}
|
||||
return GeneralUtility::camelCaseToLowerCaseUnderscored($propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of a child object.
|
||||
*
|
||||
* @param string $parentClassName The class name of the object this proxy is part of
|
||||
* @param string $propertyName The name of the proxied property in it's parent
|
||||
* @throws UnexpectedTypeException
|
||||
* @return string The class name of the child object
|
||||
*/
|
||||
public function getType($parentClassName, $propertyName)
|
||||
{
|
||||
try {
|
||||
$primaryType = $this->reflectionService
|
||||
->getClassSchema($parentClassName)
|
||||
->getProperty($propertyName)
|
||||
->getPrimaryType();
|
||||
|
||||
if ($primaryType === null) {
|
||||
throw NoPropertyTypesException::create($parentClassName, $propertyName);
|
||||
}
|
||||
|
||||
if ($primaryType->isCollection() && $primaryType->getCollectionValueTypes() !== []) {
|
||||
$primaryCollectionValueType = $primaryType->getCollectionValueTypes()[0];
|
||||
return $primaryCollectionValueType->getClassName()
|
||||
?? $primaryCollectionValueType->getBuiltinType();
|
||||
}
|
||||
|
||||
return $primaryType->getClassName()
|
||||
?? $primaryType->getBuiltinType();
|
||||
} catch (NoSuchPropertyException|NoPropertyTypesException $e) {
|
||||
}
|
||||
|
||||
throw new UnexpectedTypeException('Could not determine the child object type.', 1251315967);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a plain value, i.e. objects are flattened out if possible.
|
||||
* Multi value objects or arrays will be converted to a comma-separated list for use in "IN" SQL queries.
|
||||
* Caution: We do not return "null" values yet, if so, we need to adapt all places to handle null (see git history of this line)
|
||||
*
|
||||
* @param mixed $input The value that will be converted.
|
||||
* @param ColumnMap|null $columnMap Optional column map for retrieving the date storage format.
|
||||
*/
|
||||
public function getPlainValue(mixed $input, ?ColumnMap $columnMap = null): int|string
|
||||
{
|
||||
if ($input instanceof \DateTimeInterface || ($input === null && $columnMap?->type === TableColumnType::DATETIME)) {
|
||||
return QueryHelper::transformDateTimeToDatabaseValue(
|
||||
$input,
|
||||
$columnMap->isNullable ?? false,
|
||||
$columnMap->dateTimeFormat ?? 'datetime',
|
||||
$columnMap?->dateTimeStorageFormat
|
||||
) ?? 'NULL';
|
||||
}
|
||||
|
||||
if ($input === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if ($input instanceof \BackedEnum) {
|
||||
return $input->value;
|
||||
}
|
||||
|
||||
if ($input instanceof LazyLoadingProxy) {
|
||||
$input = $input->_loadRealInstance();
|
||||
}
|
||||
|
||||
if (is_bool($input)) {
|
||||
return (int)$input;
|
||||
}
|
||||
|
||||
if (is_int($input)) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
if ($input instanceof Country) {
|
||||
// @todo Check if this can be abstracted in a better way (for future TCA types)
|
||||
return $input->getAlpha2IsoCode();
|
||||
}
|
||||
|
||||
if ($input instanceof DomainObjectInterface) {
|
||||
return (int)$input->getUid();
|
||||
}
|
||||
|
||||
if (TypeHandlingUtility::isValidTypeForMultiValueComparison($input)) {
|
||||
$plainValueArray = [];
|
||||
foreach ($input as $inputElement) {
|
||||
$plainValueArray[] = $this->getPlainValue($inputElement, $columnMap);
|
||||
}
|
||||
return implode(',', $plainValueArray);
|
||||
}
|
||||
|
||||
if (is_object($input)) {
|
||||
if (TypeHandlingUtility::isCoreType($input) || $input instanceof \Stringable) {
|
||||
return (string)$input;
|
||||
}
|
||||
|
||||
throw new UnexpectedTypeException('An object of class "' . get_class($input) . '" could not be converted to a plain value.', 1274799934);
|
||||
}
|
||||
|
||||
return (string)$input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
|
||||
|
||||
class Exception extends \TYPO3\CMS\Extbase\Persistence\Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
|
||||
|
||||
class NonExistentPropertyException extends Exception {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
|
||||
|
||||
class UnknownPropertyTypeException extends Exception {}
|
||||
Reference in New Issue
Block a user