TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,79 @@
<?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\Core\Domain\Access;
use Psr\EventDispatcher\StoppableEventInterface;
use TYPO3\CMS\Core\Context\Context;
/**
* Event to modify records to be checked against "enableFields".
* Listeners are able to grant access or to modify the record itself to
* continue to use the native access check functionality with a modified dataset.
*/
final class RecordAccessGrantedEvent implements StoppableEventInterface
{
private ?bool $accessGranted = null;
public function __construct(
private readonly string $tableName,
private array $record,
private readonly Context $context
) {}
public function isPropagationStopped(): bool
{
return $this->accessGranted !== null;
}
/**
* @internal
*/
public function accessGranted(): bool
{
if ($this->accessGranted === null) {
throw new \RuntimeException('Access was not yet defined.', 1645506529);
}
return $this->accessGranted;
}
public function setAccessGranted(bool $accessGranted): void
{
$this->accessGranted = $accessGranted;
}
public function getTable(): string
{
return $this->tableName;
}
public function getRecord(): array
{
return $this->record;
}
public function updateRecord(array $record): void
{
$this->record = $record;
}
public function getContext(): Context
{
return $this->context;
}
}
+143
View File
@@ -0,0 +1,143 @@
<?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\Core\Domain\Access;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
/**
* Checks if a record can be accessed (usually in TYPO3 Frontend) due to various "enableFields" or group access checks.
*
* Not related to "write permissions" etc.
*/
#[Autoconfigure(public: true)]
readonly class RecordAccessVoter
{
public function __construct(
protected EventDispatcherInterface $eventDispatcher,
protected TcaSchemaFactory $tcaSchemaFactory,
) {}
/**
* Checks page record for enableFields
* Returns TRUE if enableFields does not disable the page record.
* Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation
*
* @param string $table the TCA table to check for
* @param array $record The record to evaluate (needs fields: hidden, starttime, endtime, fe_group)
* @param Context $context Context API to check against
* @return bool TRUE, if record is viewable.
*/
public function accessGranted(string $table, array $record, Context $context): bool
{
$event = new RecordAccessGrantedEvent($table, $record, $context);
$this->eventDispatcher->dispatch($event);
if ($event->isPropagationStopped()) {
return $event->accessGranted();
}
$record = $event->getRecord();
$schema = $this->tcaSchemaFactory->get($table);
$visibilityAspect = $context->getAspect('visibility');
$includeHidden = $table === 'pages'
? $visibilityAspect->includeHiddenPages()
: $visibilityAspect->includeHiddenContent();
// Hidden field is active and hidden records should not be included
if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
if (($record[$fieldName] ?? false) && !$includeHidden) {
return false;
}
}
// Records' starttime set AND is HIGHER than the current access time
if ($schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName();
if (isset($record[$fieldName])
&& (int)$record[$fieldName] > $GLOBALS['SIM_ACCESS_TIME']
&& !$visibilityAspect->includeScheduledRecords()
) {
return false;
}
}
// Records' endtime is set AND NOT "0" AND LOWER than the current access time
if ($schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
if (isset($record[$fieldName])
&& ((int)$record[$fieldName] !== 0)
&& ((int)$record[$fieldName] < $GLOBALS['SIM_ACCESS_TIME'])
&& !$visibilityAspect->includeScheduledRecords()
) {
return false;
}
}
// Insufficient group access
if ($this->groupAccessGranted($table, $record, $context) === false) {
return false;
}
// Record is available
return true;
}
/**
* Check group access against a record, if the current users' groups match the fe_group values of the record.
*
* @param string $table the TCA table to check for
* @param array $record The record to evaluate (needs enableField: fe_group)
* @param Context $context Context API to check against
* @return bool TRUE, if group access is granted.
*/
public function groupAccessGranted(string $table, array $record, Context $context): bool
{
if (!$this->tcaSchemaFactory->has($table)) {
return true;
}
$schema = $this->tcaSchemaFactory->get($table);
// No fe_group field in TCA, so no group access check
if (!$schema->hasCapability(TcaSchemaCapability::RestrictionUserGroup)) {
return true;
}
$fieldName = $schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)->getFieldName();
// Field not given, so no group access check
if (!($record[$fieldName] ?? false)) {
return true;
}
// No frontend user, but 'fe_group' is not empty, so shut this down.
if (!$context->hasAspect('frontend.user')) {
return false;
}
$pageGroupList = explode(',', (string)$record[$fieldName]);
return count(array_intersect($context->getAspect('frontend.user')->getGroupIds(), $pageGroupList)) > 0;
}
/**
* Checks if the current page of the root line is visible.
*
* If the field extendToSubpages is 0, access is granted,
* else the fields hidden, starttime, endtime, fe_group are evaluated.
*
* @internal this is a special use case and should only be used with care, not part of TYPO3's Public API.
*/
public function accessGrantedForPageInRootLine(array $pageRecord, Context $context): bool
{
return !($pageRecord['extendToSubpages'] ?? false) || $this->accessGranted('pages', $pageRecord, $context);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?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\Core\Domain;
/**
* String wrapper that keeps track of how often the value was consumed.
* This can be used to make decisions during runtime, depending on whether
* a provided value actually has been used (e.g. in rendered content).
*/
class ConsumableString implements \Countable, \Stringable
{
/**
* @internal use the `consume()` method instead
*/
public readonly string $value;
private int $counter = 0;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->consume();
}
public function count(): int
{
return $this->counter;
}
public function consume(): string
{
$this->counter++;
return $this->value;
}
}
+147
View File
@@ -0,0 +1,147 @@
<?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\Core\Domain;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* @internal
*/
final readonly class DateTimeFactory
{
public static function createFromDatabaseValue(int|string|null $value, DateTimeFieldType $fieldInformation): ?\DateTimeImmutable
{
return self::fromDatabase(
$value,
$fieldInformation->isNullable(),
$fieldInformation->getFormat(),
$fieldInformation->getPersistenceType(),
);
}
public static function createFromDatabaseValueAndTCAConfig(int|string|null $value, array $fieldConfig): ?\DateTimeImmutable
{
$persistenceType = in_array($fieldConfig['dbType'] ?? null, QueryHelper::getDateTimeTypes(), true) ? $fieldConfig['dbType'] : null;
$isNative = $persistenceType !== null;
$isNullable = (bool)($fieldConfig['nullable'] ?? $isNative);
$format = self::getFormatFromTCAConfig($fieldConfig);
return self::fromDatabase(
$value,
$isNullable,
$format,
$persistenceType
);
}
public static function getFormatFromTCAConfig(array $fieldConfig): string
{
$format = $fieldConfig['format'] ?? null;
$persistenceType = in_array($fieldConfig['dbType'] ?? null, QueryHelper::getDateTimeTypes(), true) ? $fieldConfig['dbType'] : null;
// A native time field must not be formatted as date
if (($format === 'datetime' || $format === 'datetimesec' || $format === 'date') && $persistenceType === 'time') {
return 'timesec';
}
// A native date field must not be formatted as time
if (($format === 'time' || $format === 'timesec' || $format === 'datetime' || $format === 'datetimesec') && $persistenceType === 'date') {
return 'date';
}
if (in_array($format, ['datetime', 'date', 'time', 'timesec', 'datetimesec'], true)) {
return $format;
}
if ($persistenceType !== null) {
return $persistenceType === 'time' ? 'timesec' : $persistenceType;
}
return 'datetime';
}
/**
* Create a DateTimeImmutable object from a unix timestamp in server localtime
*
* Alternative to \DateTimeImmutable('@…') which forces UTC timezone
*/
public static function createFromTimestamp(int $timestamp): \DateTimeImmutable
{
// Create a new DateTime object in current timezone
//
// Note: As documented by PHP, `\DateTime` or `\DateTimeImmutable`
// objects created from timestamps (e.g., '@12345678') as the
// first constructor argument will use UTC as timezone instead of localtime,
// therefore we must not initialize with a timestamp directly.
$datetime = new \DateTimeImmutable();
// Apply timestamp (which will not change the objects timezone)
return $datetime->setTimestamp($timestamp);
}
private static function fromDatabase(
int|string|null $value,
bool $isNullable,
string $format,
?string $persistenceType
): ?\DateTimeImmutable {
if ($value === null || $value === '') {
return null;
}
$emptyFormat = QueryHelper::getDateTimeFormats()[$persistenceType ?? '']['empty'] ?? null;
// A regular empty value is null for nullable fields
$emptyValue = $isNullable ? null : ($emptyFormat ?? 0);
// A legacy empty value is "0000-00-00" or "0000-00-00 00:00:00" stored
// in a nullable native DATE or DATETIME field (which should already use
// a proper `null` value, but still has a legacy empty value set).
$legacyEmptyValue = $persistenceType === 'date' || $persistenceType === 'datetime' ? $emptyFormat : null;
if (MathUtility::canBeInterpretedAsInteger($value)) {
$value = (int)$value;
}
if ($value === $emptyValue || $value === $legacyEmptyValue) {
return null;
}
try {
$datetime = match (true) {
is_int($value) && ($format === 'time' || $format === 'timesec') => new \DateTimeImmutable(
// time(sec) is stored as elapsed seconds in DB and has no defined date associated.
// Per convention we map to 1970-01-01 for the sake of a reliable date.
// We still want a PHP localtime timezone in the DateTime object set,
// therefore we interpret the second as UTC time on 1970-01-01T00:00:00
// and map the resulting value to PHP localtime
gmdate(DateTimeFormat::ISO8601_LOCALTIME, $value)
),
// Unix timestamp
is_int($value) => self::createFromTimestamp($value),
// The database always contains server localtime in native fields.
// The field value is something like "2016-01-01" or "2016-01-01 10:11:12.
default => new \DateTimeImmutable($value),
};
} catch (\DateMalformedStringException $e) {
throw new \InvalidArgumentException('Invalid date provided', 1743159490, $e);
}
return match ($format) {
// time(sec) is stored as elapsed seconds in DB, hence we normalize it as time on 1970-01-01 for consistency
'time' => $datetime->setDate(1970, 1, 1)->setTime((int)$datetime->format('H'), (int)$datetime->format('i'), 0),
'timesec' => $datetime->setDate(1970, 1, 1),
'date' => $datetime->setTime(0, 0, 0),
// default case also for 'datetimesec'
default => $datetime,
};
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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\Core\Domain;
/**
* @internal
*/
final readonly class DateTimeFormat
{
// Like \DateTimeInterface::ATOM but without timezone offset,
// e.g. 2005-08-15T15:52:01
// Links:
// * https://en.wikipedia.org/wiki/ISO_8601#Local_time_(unqualified)
// * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#local-dates-and-times
// * https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local)
public const string ISO8601_LOCALTIME = 'Y-m-d\\TH:i:s';
}
+28
View File
@@ -0,0 +1,28 @@
<?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\Core\Domain;
/**
* Interface that indicated an object can be compared for equality with other instances.
*
* @internal not part of public API
*/
interface EqualityInterface
{
public function equals(EqualityInterface $other): bool;
}
@@ -0,0 +1,68 @@
<?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\Core\Domain\Event;
use TYPO3\CMS\Core\Context\LanguageAspect;
/**
* Event which is fired after a record was translated (or tried to be localized).
*/
final class AfterRecordLanguageOverlayEvent
{
public function __construct(
private readonly string $table,
private readonly array $record,
private ?array $localizedRecord,
private bool $overlayingWasAttempted,
private readonly LanguageAspect $languageAspect
) {}
public function getTable(): string
{
return $this->table;
}
public function getRecord(): array
{
return $this->record;
}
public function getLanguageAspect(): LanguageAspect
{
return $this->languageAspect;
}
public function setLocalizedRecord(?array $localizedRecord): void
{
$this->overlayingWasAttempted = true;
$this->localizedRecord = $localizedRecord;
}
public function getLocalizedRecord(): ?array
{
return $this->localizedRecord;
}
/**
* Determines if the overlay functionality happened, thus, returning the lo
*/
public function overlayingWasAttempted(): bool
{
return $this->overlayingWasAttempted;
}
}
@@ -0,0 +1,83 @@
<?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\Core\Domain\Event;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Domain\Page;
/**
* Event which is fired before a page (id) is being resolved from PageRepository.
*
* Allows to change the corresponding page ID, e.g. to resolve a different page
* with custom overlaying, or to fully resolve the page on your own.
*/
final class BeforePageIsRetrievedEvent
{
private ?Page $page = null;
public function __construct(
private int $pageId,
private bool $skipGroupAccessCheck,
private readonly Context $context,
) {}
public function getPage(): ?Page
{
return $this->page;
}
public function setPage(Page $page): void
{
$this->page = $page;
}
public function hasPage(): bool
{
return $this->page !== null;
}
public function getPageId(): int
{
return $this->pageId;
}
public function setPageId(int $pageId): void
{
$this->pageId = $pageId;
}
public function skipGroupAccessCheck(): void
{
$this->skipGroupAccessCheck = true;
}
public function respectGroupAccessCheck(): void
{
$this->skipGroupAccessCheck = false;
}
public function isGroupAccessCheckSkipped(): bool
{
return $this->skipGroupAccessCheck;
}
public function getContext(): Context
{
return $this->context;
}
}
@@ -0,0 +1,63 @@
<?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\Core\Domain\Event;
use TYPO3\CMS\Core\Context\LanguageAspect;
/**
* Event which is fired before a single page or a list of
* pages are about to be translated (or tried to be localized).
*/
final class BeforePageLanguageOverlayEvent
{
public function __construct(
private array $pageInput,
private array $pageIds,
private LanguageAspect $languageAspect
) {}
public function getPageInput(): array
{
return $this->pageInput;
}
public function setPageInput(array $pageInput): void
{
$this->pageInput = $pageInput;
}
public function getPageIds(): array
{
return $this->pageIds;
}
public function setPageIds(array $pageIds): void
{
$this->pageIds = array_map(intval(...), $pageIds);
}
public function getLanguageAspect(): LanguageAspect
{
return $this->languageAspect;
}
public function setLanguageAspect(LanguageAspect $languageAspect): void
{
$this->languageAspect = $languageAspect;
}
}
@@ -0,0 +1,58 @@
<?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\Core\Domain\Event;
use TYPO3\CMS\Core\Context\LanguageAspect;
/**
* Event which is fired before a record in a language should be "language overlaid",
* that is: Finding a translation for a given record.
*/
final class BeforeRecordLanguageOverlayEvent
{
public function __construct(
private readonly string $table,
private array $record,
private LanguageAspect $languageAspect
) {}
public function getTable(): string
{
return $this->table;
}
public function getRecord(): array
{
return $this->record;
}
public function setRecord(array $record): void
{
$this->record = $record;
}
public function getLanguageAspect(): LanguageAspect
{
return $this->languageAspect;
}
public function setLanguageAspect(LanguageAspect $languageAspect): void
{
$this->languageAspect = $languageAspect;
}
}
@@ -0,0 +1,83 @@
<?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\Core\Domain\Event;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
/**
* Event which is fired when compiling the list of constraints such as "deleted" and "starttime",
* "endtime" etc.
*
* This Event allows for additional enableColumns to be added or removed to the list of constraints.
*
* An example: The extension ingmar_accessctrl enables assigning more
* than one usergroup to content and page records
*/
final class ModifyDefaultConstraintsForDatabaseQueryEvent
{
public function __construct(
private readonly string $table,
private readonly string $tableAlias,
private readonly ExpressionBuilder $expressionBuilder,
/** @var array<string, CompositeExpression|string> */
private array $constraints,
/** @var array<string, bool> */
private readonly array $enableFieldsToIgnore,
private readonly Context $context
) {}
public function getTable(): string
{
return $this->table;
}
public function getTableAlias(): string
{
return $this->tableAlias;
}
public function getExpressionBuilder(): ExpressionBuilder
{
return $this->expressionBuilder;
}
/**
* @return array<string, CompositeExpression|string>
*/
public function getConstraints(): array
{
return $this->constraints;
}
public function setConstraints(array $constraints): void
{
$this->constraints = $constraints;
}
public function getEnableFieldsToIgnore(): array
{
return array_keys(array_filter($this->enableFieldsToIgnore));
}
public function getContext(): Context
{
return $this->context;
}
}
@@ -0,0 +1,123 @@
<?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\Core\Domain\Event;
use Psr\EventDispatcher\StoppableEventInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
use TYPO3\CMS\Core\Domain\RawRecord;
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Schema\TcaSchema;
/**
* Event which allows to manipulate the properties to be used for a new Record.
* With this event, it's even possible to create a new Record manually.
*/
final class RecordCreationEvent implements StoppableEventInterface
{
public function __construct(
private array $properties,
private readonly RawRecord $rawRecord,
private readonly SystemProperties $systemProperties,
private readonly Context $context,
private readonly RecordIdentityMap $recordIdentityMap,
private readonly TcaSchema $schema,
private ?RecordInterface $record = null,
) {}
public function setRecord(RecordInterface $record): void
{
$this->record = $record;
}
public function isPropagationStopped(): bool
{
return $this->record !== null;
}
public function hasProperty(string $name): bool
{
return array_key_exists($name, $this->properties);
}
public function setProperty(string $name, mixed $propertyValue): void
{
$this->properties[$name] = $propertyValue;
}
public function setProperties(array $properties): void
{
$this->properties = $properties;
}
public function unsetProperty(string $name): bool
{
if (!$this->hasProperty($name)) {
return false;
}
unset($this->properties[$name]);
return true;
}
public function getProperty(string $name): mixed
{
return $this->properties[$name] ?? null;
}
public function getProperties(): array
{
return $this->properties;
}
public function getRawRecord(): RawRecord
{
return $this->rawRecord;
}
public function getSystemProperties(): SystemProperties
{
return $this->systemProperties;
}
public function getContext(): Context
{
return $this->context;
}
public function getRecordIdentityMap(): RecordIdentityMap
{
return $this->recordIdentityMap;
}
/**
* If available this is the subSchema for the current record type
*/
public function getSchema(): TcaSchema
{
return $this->schema;
}
/**
* @internal
*/
public function getRecord(): ?RecordInterface
{
return $this->record;
}
}
@@ -0,0 +1,26 @@
<?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\Core\Domain\Exception;
use Psr\Container\ContainerExceptionInterface;
use TYPO3\CMS\Core\Exception;
/**
* @internal
*/
class FlexFieldPropertyException extends Exception implements ContainerExceptionInterface {}
@@ -0,0 +1,26 @@
<?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\Core\Domain\Exception;
use Psr\Container\NotFoundExceptionInterface;
use TYPO3\CMS\Core\Exception;
/**
* @internal
*/
class FlexFieldPropertyNotFoundException extends Exception implements NotFoundExceptionInterface {}
@@ -0,0 +1,25 @@
<?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\Core\Domain\Exception;
use TYPO3\CMS\Core\Exception;
/**
* @internal
*/
class IncompleteRecordException extends Exception {}
@@ -0,0 +1,26 @@
<?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\Core\Domain\Exception;
use Psr\Container\ContainerExceptionInterface;
use TYPO3\CMS\Core\Exception;
/**
* @internal
*/
class RecordPropertyException extends Exception implements ContainerExceptionInterface {}
@@ -0,0 +1,26 @@
<?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\Core\Domain\Exception;
use Psr\Container\NotFoundExceptionInterface;
use TYPO3\CMS\Core\Exception;
/**
* @internal
*/
class RecordPropertyNotFoundException extends Exception implements NotFoundExceptionInterface {}
+173
View File
@@ -0,0 +1,173 @@
<?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\Core\Domain;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Domain\Exception\FlexFieldPropertyException;
use TYPO3\CMS\Core\Domain\Exception\FlexFieldPropertyNotFoundException;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* Represents a record's flex form field values.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
class FlexFormFieldValues implements ContainerInterface, \ArrayAccess
{
public function __construct(
protected array $sheets
) {}
public function get(string $id)
{
if (!$this->has($id)) {
throw new FlexFieldPropertyNotFoundException('Flex property "' . $id . '" is not available.', 1731962637);
}
[$sheetName, $propertyPath] = $this->processId($id);
if ($sheetName === '' && $this->hasMultipleSheets()) {
// Get the sheet name for the requested property path - There is one, since has() returned true.
foreach ($this->sheets as $name => $sheet) {
if (ArrayUtility::isValidPath($sheet, $propertyPath, '.')) {
$sheetName = $name;
break;
}
}
}
$propertyValue = ArrayUtility::getValueByPath($this->sheets[$sheetName], $propertyPath, '.');
if (is_array($propertyValue)) {
array_walk_recursive($propertyValue, fn(mixed &$value): mixed => $value = $this->resolveRecordPropertyClosure($propertyPath, $value));
} else {
$propertyValue = $this->resolveRecordPropertyClosure($propertyPath, $propertyValue);
}
ArrayUtility::setValueByPath($this->sheets[$sheetName], $propertyPath, $propertyValue, '.');
return $propertyValue;
}
public function has(string $id): bool
{
// If there are no sheets, no value can be determined.
if ($this->sheets === []) {
return false;
}
[$sheetName, $propertyPath] = $this->processId($id);
if ($sheetName !== '' && !isset($this->sheets[$sheetName])) {
// Given sheet name does not exist
return false;
}
if ($this->hasMultipleSheets()) {
if ($sheetName !== '') {
return ArrayUtility::isValidPath($this->sheets[$sheetName], $propertyPath, '.');
}
// In case no sheet name is given, we try to execute fallback handling
// by searching for the requested $propertyPath in all sheets.
$occurences = [];
foreach ($this->sheets as $sheetName => $sheet) {
if (ArrayUtility::isValidPath($sheet, $propertyPath, '.')) {
$occurences[$sheetName] = $propertyPath;
}
}
if (count($occurences) > 1) {
// This is a special case, which we handle with an exception to create awareness for the error.
throw new FlexFieldPropertyException('Given id is ambigious since the field exists in multiple sheets and no sheet is defined.', 1731962638);
}
// Whether the requested $propertyPath name exist in a sheet
return count($occurences) === 1;
}
// Standard case, whether the $propertyPath exists in the given sheet name
return ArrayUtility::isValidPath($this->sheets[$sheetName], $propertyPath, '.');
}
public function offsetExists(mixed $offset): bool
{
return $this->has($offset);
}
public function offsetGet(mixed $offset): mixed
{
return $this->get($offset);
}
public function offsetSet(mixed $offset, mixed $value): void
{
// Not implemented
}
public function offsetUnset(mixed $offset): void
{
// Not implemented
}
/**
* Getter to be used in fluid for accessing field values
*/
public function getSheets(): array
{
return $this->sheets;
}
public function toArray(): array
{
return $this->getSheets();
}
protected function hasMultipleSheets(): bool
{
return count($this->sheets) > 1;
}
protected function processId(string $id): array
{
if (str_contains($id, '/')) {
// $id contains a sheet name
return explode('/', $id, 2);
}
if ($this->hasMultipleSheets()) {
// $id does not contain a sheet name while there are multiple sheets. Therefore, we
// return an empty sheet name. This allows executing fallback handling in has() and get().
return ['', $id];
}
// In case the $id does not contain a sheet name, but we have a
// single sheet flex form, we fall back to this name automatically.
return [key($this->sheets), $id];
}
protected function resolveRecordPropertyClosure(string $id, mixed $propertyValue): mixed
{
if ($propertyValue instanceof RecordPropertyClosure) {
try {
$propertyValue = $propertyValue->instantiate();
} catch (\Exception $e) {
// Consumers of this method can rely on catching ContainerExceptionInterface
throw new FlexFieldPropertyException(
'An exception occurred while instantiating flex field property "' . $id . '"',
1731962735,
$e
);
}
}
return $propertyValue;
}
}
+218
View File
@@ -0,0 +1,218 @@
<?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\Core\Domain;
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
/**
* @internal not part of public API, as this needs to be streamlined and proven
*/
class Page extends Record implements \ArrayAccess
{
protected array $specialPropertyNames = [
'_language',
'_LOCALIZED_UID',
'_REQUESTED_OVERLAY_LANGUAGE',
'_MP_PARAM',
'_ORIG_uid',
'_ORIG_pid',
'_SHORTCUT_ORIGINAL_PAGE_UID',
'_TRANSLATION_SOURCE',
];
protected array $specialProperties = [];
/**
* @param RawRecord|array $rawRecordOrProperties RawRecord when created via RecordFactory, array for legacy usage
*/
public function __construct(RawRecord|array $rawRecordOrProperties, array $properties = [], ?SystemProperties $systemProperties = null)
{
if ($rawRecordOrProperties instanceof RawRecord) {
parent::__construct($rawRecordOrProperties, $properties, $systemProperties);
$this->extractSpecialPropertiesFromComputed($rawRecordOrProperties);
} else {
$this->initFromArray($rawRecordOrProperties);
}
}
public function has(string $id): bool
{
if (parent::has($id)) {
return true;
}
return array_key_exists($id, $this->specialProperties);
}
public function get(string $id): mixed
{
if (parent::has($id)) {
return parent::get($id);
}
if (array_key_exists($id, $this->specialProperties)) {
return $this->specialProperties[$id];
}
throw new RecordPropertyNotFoundException('Record property "' . $id . '" is not available.', 1725892141);
}
public function getLanguageId(): int
{
if ($this->systemProperties?->getLanguage() !== null) {
return $this->systemProperties->getLanguage()->getLanguageId();
}
return (int)($this->specialProperties['_language'] ?? $this->properties['language_tag'] ?? 0);
}
public function getPageId(): int
{
if ($this->systemProperties?->getLanguage() !== null) {
$translationParent = $this->systemProperties->getLanguage()->getTranslationParent();
return $translationParent > 0 ? $translationParent : $this->getUid();
}
$pageId = isset($this->properties['l10n_parent']) && $this->properties['l10n_parent'] > 0 ? $this->properties['l10n_parent'] : $this->getUid();
return (int)$pageId;
}
public function getTranslationSource(): ?Page
{
return $this->specialProperties['_TRANSLATION_SOURCE'] ?? null;
}
public function getRequestedLanguage(): ?int
{
return $this->specialProperties['_REQUESTED_OVERLAY_LANGUAGE'] ?? null;
}
public function toArray(bool $includeSystemProperties = false): array
{
if ($includeSystemProperties) {
// When including system properties, return the full raw record overlaid
// with resolved properties and special properties for backward compatibility.
$result = $this->rawRecord->toArray();
foreach ($this->properties as $key => $property) {
if ($property instanceof RecordPropertyClosure) {
$this->properties[$key] = $property->instantiate();
}
$result[$key] = $this->properties[$key];
}
$result += ['_system' => $this->systemProperties?->toArray() ?? []];
$result += $this->specialProperties;
return $result;
}
return parent::toArray();
}
public function offsetExists(mixed $offset): bool
{
return $this->has((string)$offset);
}
public function offsetGet(mixed $offset): mixed
{
return $this->has((string)$offset) ? $this->get((string)$offset) : null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->properties[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->properties[$offset]);
}
private function extractSpecialPropertiesFromComputed(RawRecord $rawRecord): void
{
$computedProperties = $rawRecord->getComputedProperties();
if ($computedProperties->getLocalizedUid() !== null) {
$this->specialProperties['_LOCALIZED_UID'] = $computedProperties->getLocalizedUid();
}
if ($computedProperties->getRequestedOverlayLanguageId() !== null) {
$this->specialProperties['_REQUESTED_OVERLAY_LANGUAGE'] = $computedProperties->getRequestedOverlayLanguageId();
}
if ($computedProperties->getTranslationSource() !== null) {
$this->specialProperties['_TRANSLATION_SOURCE'] = $computedProperties->getTranslationSource();
}
if ($computedProperties->getVersionedUid() !== null) {
$this->specialProperties['_ORIG_uid'] = $computedProperties->getVersionedUid();
}
// Extract remaining special properties from the raw record
$rawProperties = $rawRecord->toArray();
foreach ($this->specialPropertyNames as $name) {
if (isset($rawProperties[$name]) && !isset($this->specialProperties[$name])) {
$this->specialProperties[$name] = $rawProperties[$name];
}
}
}
private function initFromArray(array $properties): void
{
$regularProperties = [];
$translationSource = null;
$localizedUid = null;
$versionedUid = null;
$requestedOverlayLanguageId = null;
foreach ($properties as $propertyName => $propertyValue) {
if (in_array($propertyName, $this->specialPropertyNames)) {
if ($propertyName === '_TRANSLATION_SOURCE' && !$propertyValue instanceof Page) {
$translationSource = new Page($propertyValue);
$this->specialProperties[$propertyName] = $translationSource;
} elseif ($propertyName === '_TRANSLATION_SOURCE') {
$translationSource = $propertyValue;
$this->specialProperties[$propertyName] = $propertyValue;
} elseif ($propertyName === '_LOCALIZED_UID') {
$localizedUid = $propertyValue;
$this->specialProperties[$propertyName] = $propertyValue;
} elseif ($propertyName === '_ORIG_uid') {
$versionedUid = $propertyValue;
$this->specialProperties[$propertyName] = $propertyValue;
} elseif ($propertyName === '_REQUESTED_OVERLAY_LANGUAGE') {
$requestedOverlayLanguageId = $propertyValue;
$this->specialProperties[$propertyName] = $propertyValue;
} else {
$this->specialProperties[$propertyName] = $propertyValue;
}
} else {
$regularProperties[$propertyName] = $propertyValue;
}
}
$computedProperties = new ComputedProperties(
versionedUid: $versionedUid,
localizedUid: $localizedUid,
requestedOverlayLanguageId: $requestedOverlayLanguageId,
translationSource: $translationSource
);
$recordType = isset($regularProperties['doktype']) ? (string)$regularProperties['doktype'] : null;
$fullType = $recordType !== null ? 'pages.' . $recordType : 'pages';
$rawRecord = new RawRecord(
uid: (int)($regularProperties['uid'] ?? 0),
pid: (int)($regularProperties['pid'] ?? 0),
properties: $regularProperties,
computedProperties: $computedProperties,
fullType: $fullType
);
parent::__construct($rawRecord, $regularProperties, null);
}
}
@@ -0,0 +1,238 @@
<?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\Core\Domain\Persistence;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\DateTimeAspect;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Context\VisibilityAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fetches all records of a single table of one / multiple PID(s)
* in one DB query, and stores them in a runtime cache.
*
* The records are neither grouped nor do we care about
* - sorting
* - limit / offset
* - or BE User permissions.
*
* It is "greedy" because it is meant to do simple query
* in a fast way, to reduce overlays on a "per record" basis.
*
* The records are fetched depending on
* - fe_group permissions
* - language (incl. overlays etc)
* - workspace
* - visibility restrictions (starttime / endtime / hidden / deleted)
*
* The class returns an array and does not handle objects,
* as it is very "lowlevel" and thus: internal.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
readonly class GreedyDatabaseBackend
{
public function __construct(
#[Autowire(service: 'cache.runtime')]
protected FrontendInterface $runtimeCache,
protected RecordAccessVoter $recordAccessVoter,
protected ConnectionPool $connectionPool
) {}
public function getRows(string $tableName, array $uids, Context $context): array
{
$cacheIdentifier = $this->createRuntimeCacheIdentifier($tableName, $uids, $context);
$allRows = $this->getRowsFromCache($cacheIdentifier, $tableName, $uids, $context);
if ($allRows === null) {
$allRows = $this->getRowsFromDatabase($tableName, $uids, $context);
$this->setCache($cacheIdentifier, $tableName, $context, $allRows);
}
return $this->handleOverlays(
// Only use the records from the given UIDs
array_filter($allRows, static fn(array $row) => in_array((int)$row['uid'], $uids, true)),
$tableName,
$context
);
}
protected function setCache(string $cacheIdentifier, string $tableName, Context $context, array $allRows): void
{
$resultUids = array_map(fn(array $row): int => (int)$row['uid'], $allRows);
foreach ($resultUids as $resultUid) {
$resultUidCacheIdentifier = $this->createRuntimeCacheIdentifier($tableName, [$resultUid], $context, 'pointer');
// Set pointer to actual rows cache entry.
$this->runtimeCache->set($resultUidCacheIdentifier, $cacheIdentifier);
}
$this->runtimeCache->set($cacheIdentifier, $allRows);
}
/**
* This method creates cache identifier pointers for each provided uid.
* These uids come from the same pid, so they will have the same result set.
* If at a later point in time one of these uids is requested again,
* the pointer will be used to retrieve the actual cache entry of the db row.
* Without this mechanism, the runtime cache would be filled quickly with the
* same database rows over and over again.
*
* Example: Having 1000 records on the same pid with each having a relation to
* a file reference. When RecordFactory is used to resolve all of these 1000 records
* at a time, each of these 1000 relations would produce a cache entry with 1000
* file reference database rows (1000*1000 = 1.000.000 database rows).
*
* Instead, only 1 cache entry is created with 1000 database rows and in addition
* 1000 lightweight cache identifier pointers, pointing to the actual value of the
* cache identifier.
*/
protected function getRowsFromCache(string $cacheIdentifier, string $tableName, array $uids, Context $context): ?array
{
if ($this->runtimeCache->has($cacheIdentifier)) {
return $this->runtimeCache->get($cacheIdentifier);
}
foreach ($uids as $uid) {
$cacheIdentifierPointer = $this->createRuntimeCacheIdentifier($tableName, [$uid], $context, 'pointer');
if ($this->runtimeCache->has($cacheIdentifierPointer)) {
$cacheIdentifier = $this->runtimeCache->get($cacheIdentifierPointer);
if ($this->runtimeCache->has($cacheIdentifier)) {
return $this->runtimeCache->get($cacheIdentifier);
}
}
}
return null;
}
protected function getRowsFromDatabase(string $tableName, array $uids, Context $context): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder
->select('*')
->from($tableName);
// @todo: consider a context-based query restriction container here!
// @todo: we should not remove the restrictions but rather add them based on the given Context
/** @var DefaultRestrictionContainer $restrictions */
$restrictions = $queryBuilder->getRestrictions();
$visibilityAspect = $context->getAspect('visibility');
if ($visibilityAspect->includeHidden()) {
$restrictions->removeByType(HiddenRestriction::class);
}
if ($visibilityAspect->includeDeletedRecords()) {
$restrictions->removeByType(DeletedRestriction::class);
}
if ($visibilityAspect->includeScheduledRecords()) {
$restrictions->removeByType(StartTimeRestriction::class);
$restrictions->removeByType(EndTimeRestriction::class);
}
if ($context->hasAspect('frontend.user')) {
$groupIds = $context->getAspect('frontend.user')->getGroupIds();
$restrictions->add(GeneralUtility::makeInstance(FrontendGroupRestriction::class, $groupIds));
}
// Workspace Restriction is never added
$restrictions->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $context->getAspect('workspace')->getId()));
// Subselect is doing: give me the PID of the given UIDs
// So we can get a greedy query for all records of these PIDs
$queryBuilderForSubselect = $queryBuilder->getConnection()->createQueryBuilder();
// Subselect must use same restrictions as main query
$queryBuilderForSubselect->setRestrictions($restrictions);
$queryBuilderForSubselect
->select('pid')
->from($tableName)
->where(
$queryBuilderForSubselect->expr()->in(
'uid',
$queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY)
)
);
// Inject the subselect in the WHERE part of the main query
$queryBuilder->where(
$queryBuilder->expr()->comparison(
$queryBuilder->quoteIdentifier('pid'),
'IN',
'(' . $queryBuilderForSubselect->getSQL() . ')'
)
);
$allRows = $queryBuilder->executeQuery()->fetchAllAssociative();
return $allRows;
}
protected function handleOverlays(array $rows, string $dbTable, Context $context): array
{
$pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
$finalRows = [];
foreach ($rows as $row) {
$pageRepository->versionOL($dbTable, $row);
if ($row === false) {
continue;
}
$row = $pageRepository->getLanguageOverlay($dbTable, $row);
if ($row === null) {
continue;
}
// Check fe_group, hidden, starttime, endtime etc.
if (!$this->recordAccessVoter->accessGranted($dbTable, $row, $context)) {
continue;
}
$finalRows[] = $row;
}
return $finalRows;
}
protected function createRuntimeCacheIdentifier(string $tableName, array $uids, Context $context, string $suffix = ''): string
{
sort($uids);
$cacheIdentifier = $tableName . '-' . md5(implode('_', $uids)) . '-';
$cacheIdentifier .= $context->getAspect('workspace')->getId() . '-';
/** @var LanguageAspect $languageAspect */
$languageAspect = $context->getAspect('language');
$cacheIdentifier .= $languageAspect->getId() . '-' . $languageAspect->getOverlayType() . '-' . md5(implode('_', $languageAspect->getFallbackChain())) . '-';
/** @var VisibilityAspect $visibilityAspect */
$visibilityAspect = $context->getAspect('visibility');
$cacheIdentifier .= $visibilityAspect->includeHiddenPages() ? '1' : '0';
$cacheIdentifier .= $visibilityAspect->includeHiddenContent() ? '1' : '0';
$cacheIdentifier .= $visibilityAspect->includeScheduledRecords() ? '1' : '0';
$cacheIdentifier .= $visibilityAspect->includeDeletedRecords() ? '1' : '0';
/** @var DateTimeAspect $dateAspect */
$dateAspect = $context->getAspect('date');
$cacheIdentifier .= '-' . $dateAspect->get('timestamp');
/** @var UserAspect $userAspect */
$userAspect = $context->getAspect('frontend.user');
$groupIds = $userAspect->getGroupIds();
$cacheIdentifier .= '-' . implode('_', $groupIds);
$cacheIdentifier .= '-' . $suffix;
return 'greedy_database_backend_' . hash('xxh3', $cacheIdentifier);
}
}
@@ -0,0 +1,79 @@
<?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\Core\Domain\Persistence;
use TYPO3\CMS\Core\Domain\RecordInterface;
/**
* The identity map for records is a database access design pattern used to improve
* performance by providing a context-specific, in-memory cache to prevent duplicate retrieval
* of the same object data from the database.
*
* In TYPO3 Context, an instance of the RecordIdentityMap is also shared in Frontend
* (e.g. RecordFactory) and Backend (Page Module) to know which records have been created
* already.
*
* For this reason, the identity map is shared, but needs to be explicitly shared, and
* is thus, NOT, marked as a singleton.
*
* Why? Since we deal with "overlays", we also need to keep track of
* - Language (Chain)
* - Workspace
*
* In TYPO3 Context, the identity map is especially important to avoid infinite recursions
* when resolving relations in records but allows to re-use existing objects used by this
* map.
*
* The purpose for the Identity Map for Records is currently only for reading record objects
* within one records (not writing, and not shared between requests).
*
* @internal not part of TYPO3 Core API as it is used on a low-level basis to keep state of created record objects.
*/
class RecordIdentityMap
{
/**
* @var array<int|string, RecordInterface>
*/
protected array $recordMap = [];
public function add(RecordInterface $record): void
{
$this->recordMap[$record->getMainType()][$record->getUid()] = $record;
}
public function has(RecordInterface $record): bool
{
return isset($this->recordMap[$record->getMainType()][$record->getUid()]);
}
public function findByIdentifier(string $mainType, int $identifier): RecordInterface
{
if ($this->hasIdentifier($mainType, $identifier)) {
return $this->recordMap[$mainType][$identifier];
}
throw new \InvalidArgumentException(
'Record with type "' . $mainType . '" and identifier "' . $identifier . '" not found in the Identity Map.',
1720730774
);
}
public function hasIdentifier(string $mainType, int $identifier): bool
{
return isset($this->recordMap[$mainType][$identifier]);
}
}
+119
View File
@@ -0,0 +1,119 @@
<?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\Core\Domain;
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
/**
* Holds all properties of a raw database row with unfiltered and unprocessed values.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
readonly class RawRecord implements RecordInterface
{
protected string $mainType;
protected ?string $recordType;
public function __construct(
protected int $uid,
protected int $pid,
protected array $properties,
protected ComputedProperties $computedProperties,
protected string $fullType
) {
$parts = $this->normalizeTypeParts($this->fullType);
$this->mainType = $parts[0] ?? '';
$this->recordType = $parts[1] ?? null;
}
public function getUid(): int
{
return $this->uid;
}
public function getPid(): int
{
return $this->pid;
}
public function getFullType(): string
{
return $this->fullType;
}
/**
* @return non-empty-string|null
*/
public function getRecordType(): ?string
{
return $this->recordType;
}
public function getMainType(): string
{
return $this->mainType;
}
public function toArray(bool $includeComputedProperties = false): array
{
$properties = ['uid' => $this->uid, 'pid' => $this->pid] + $this->properties;
if ($includeComputedProperties) {
$properties += ['_computed' => $this->computedProperties->toArray()];
}
return $properties;
}
public function has(string $id): bool
{
return array_key_exists($id, $this->properties);
}
public function get(string $id): mixed
{
if (!$this->has($id)) {
throw new RecordPropertyNotFoundException(
'Record property "' . $id . '" is not available.',
1725892140
);
}
return $this->properties[$id] ?? null;
}
public function getComputedProperties(): ComputedProperties
{
return $this->computedProperties;
}
public function getRawRecord(): RawRecord
{
return $this;
}
/**
* @return array{0?: string, 1?: string}
*/
protected function normalizeTypeParts(string $type): array
{
return array_filter(
array_map(trim(...), explode('.', $type, 2)),
static fn(string $part): bool => $part !== ''
);
}
}
+175
View File
@@ -0,0 +1,175 @@
<?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\Core\Domain;
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyException;
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
use TYPO3\CMS\Core\Domain\Record\LanguageInfo;
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
use TYPO3\CMS\Core\Domain\Record\VersionInfo;
/**
* Represents a record with all properties valid for this record type.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
class Record implements RecordInterface
{
public function __construct(
protected readonly RawRecord $rawRecord,
protected array $properties,
protected readonly ?SystemProperties $systemProperties = null,
) {}
public function getUid(): int
{
return $this->rawRecord->getUid();
}
public function getPid(): int
{
return $this->rawRecord->getPid();
}
public function getFullType(): string
{
return $this->rawRecord->getFullType();
}
public function getRecordType(): ?string
{
return $this->rawRecord->getRecordType();
}
public function getMainType(): string
{
return $this->rawRecord->getMainType();
}
public function toArray(bool $includeSystemProperties = false): array
{
$properties = ['uid' => $this->getUid(), 'pid' => $this->getPid()];
foreach ($this->properties as $key => $property) {
if ($property instanceof RecordPropertyClosure) {
$this->properties[$key] = $property->instantiate();
}
}
$properties += $this->properties;
if ($includeSystemProperties) {
$properties += ['_system' => $this->systemProperties?->toArray() ?? []];
}
return $properties;
}
public function has(string $id): bool
{
if (array_key_exists($id, $this->properties)) {
return true;
}
if (in_array($id, ['uid', 'pid'], true)) {
// Enable access of uid and pid via array access
return true;
}
if ($this->getRecordType() === null && $this->rawRecord->has($id)) {
// Only fall back to the raw record in case no record type is defined.
// This allows to properly check for only record type specific fields.
return true;
}
return false;
}
public function get(string $id): mixed
{
if (array_key_exists($id, $this->properties)) {
$property = $this->properties[$id];
if ($property instanceof RecordPropertyClosure) {
try {
$property = $property->instantiate();
} catch (\Exception $e) {
// Consumers of this method can rely on catching ContainerExceptionInterface
throw new RecordPropertyException(
'An exception occurred while instantiating record property "' . $id . '"',
1725892139,
$e
);
}
$this->properties[$id] = $property;
}
return $property;
}
if (in_array($id, ['uid', 'pid'], true)) {
// Enable access of uid and pid via array access
return $this->rawRecord->get($id);
}
if ($this->getRecordType() === null && $this->rawRecord->has($id)) {
// Only fall back to the raw record in case no record type is defined.
// This ensures that only record type specific fields are being returned.
return $this->rawRecord->get($id);
}
throw new RecordPropertyNotFoundException('Record property "' . $id . '" is not available.', 1725892138);
}
public function getVersionInfo(): ?VersionInfo
{
return $this->systemProperties?->getVersion();
}
public function getLanguageInfo(): ?LanguageInfo
{
return $this->systemProperties?->getLanguage();
}
public function getLanguageId(): ?int
{
return $this->systemProperties?->getLanguage()?->getLanguageId();
}
public function getSystemProperties(): ?SystemProperties
{
return $this->systemProperties;
}
public function getComputedProperties(): ComputedProperties
{
return $this->rawRecord->getComputedProperties();
}
public function getRawRecord(): RawRecord
{
return $this->rawRecord;
}
public function getOverlaidUid(): int
{
$computedProperties = $this->getComputedProperties();
if ($computedProperties->getLocalizedUid() !== null) {
return $computedProperties->getLocalizedUid();
}
if ($computedProperties->getVersionedUid() !== null) {
return $computedProperties->getVersionedUid();
}
return $this->getUid();
}
}
@@ -0,0 +1,65 @@
<?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\Core\Domain\Record;
use TYPO3\CMS\Core\Domain\Page;
/**
* Contains all information about computed properties for the current context.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
readonly class ComputedProperties
{
public function __construct(
protected ?int $versionedUid = null,
protected ?int $localizedUid = null,
protected ?int $requestedOverlayLanguageId = null,
protected ?Page $translationSource = null,
) {}
public function getVersionedUid(): ?int
{
return $this->versionedUid;
}
public function getLocalizedUid(): ?int
{
return $this->localizedUid;
}
public function getRequestedOverlayLanguageId(): ?int
{
return $this->requestedOverlayLanguageId;
}
public function getTranslationSource(): ?Page
{
return $this->translationSource;
}
public function toArray(): array
{
return [
'versionedUid' => $this->versionedUid,
'localizedUid' => $this->localizedUid,
'requestedOverlayLanguageId' => $this->requestedOverlayLanguageId,
'translationSource' => $this->translationSource,
];
}
}
+47
View File
@@ -0,0 +1,47 @@
<?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\Core\Domain\Record;
/**
* Contains all information about language for a language-aware record.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
readonly class LanguageInfo
{
public function __construct(
protected int $languageId,
protected ?int $translationParent,
protected ?int $translationSource
) {}
public function getLanguageId(): int
{
return $this->languageId;
}
public function getTranslationParent(): ?int
{
return $this->translationParent;
}
public function getTranslationSource(): ?int
{
return $this->translationSource;
}
}
+119
View File
@@ -0,0 +1,119 @@
<?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\Core\Domain\Record;
/**
* Contains all information about system-related properties for the current record.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
readonly class SystemProperties
{
public function __construct(
protected ?LanguageInfo $languageInfo,
protected ?VersionInfo $versionInfo,
protected ?bool $isDeleted,
protected ?bool $isDisabled,
protected ?bool $isLockedForEditing,
protected ?\DateTimeInterface $createdAt,
protected ?\DateTimeInterface $lastUpdatedAt,
protected ?\DateTimeInterface $publishAt,
protected ?\DateTimeInterface $publishUntil,
protected ?array $userGroupRestriction,
protected ?int $sorting,
protected ?string $description,
) {}
public function getLanguage(): ?LanguageInfo
{
return $this->languageInfo;
}
public function getVersion(): ?VersionInfo
{
return $this->versionInfo;
}
public function isDeleted(): ?bool
{
return $this->isDeleted;
}
public function isDisabled(): ?bool
{
return $this->isDisabled;
}
public function isLockedForEditing(): ?bool
{
return $this->isLockedForEditing;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function getLastUpdatedAt(): ?\DateTimeInterface
{
return $this->lastUpdatedAt;
}
public function getPublishAt(): ?\DateTimeInterface
{
return $this->publishAt;
}
public function getPublishUntil(): ?\DateTimeInterface
{
return $this->publishUntil;
}
public function getUserGroupRestriction(): ?array
{
return $this->userGroupRestriction;
}
public function getSorting(): ?int
{
return $this->sorting;
}
public function getDescription(): ?string
{
return $this->description;
}
public function toArray(): array
{
return [
'language' => $this->languageInfo,
'version' => $this->versionInfo,
'isDeleted' => $this->isDeleted,
'isDisabled' => $this->isDisabled,
'isLockedForEditing' => $this->isLockedForEditing,
'createdAt' => $this->createdAt,
'lastUpdatedAt' => $this->lastUpdatedAt,
'publishAt' => $this->publishAt,
'publishUntil' => $this->publishUntil,
'userGroupRestriction' => $this->userGroupRestriction,
'sorting' => $this->sorting,
'description' => $this->description,
];
}
}
+55
View File
@@ -0,0 +1,55 @@
<?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\Core\Domain\Record;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Contains all information about versioning for a workspace-aware record.
*
* @internal not part of public API, as this needs to be streamlined and proven
*/
readonly class VersionInfo
{
public function __construct(
protected int $workspaceId,
protected int $liveId,
protected VersionState $state,
protected int $stage,
) {}
public function getWorkspaceId(): int
{
return $this->workspaceId;
}
public function getLiveId(): int
{
return $this->liveId;
}
public function getState(): VersionState
{
return $this->state;
}
public function getStageId(): int
{
return $this->stage;
}
}
+350
View File
@@ -0,0 +1,350 @@
<?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\Core\Domain;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer;
use TYPO3\CMS\Core\Domain\Event\RecordCreationEvent;
use TYPO3\CMS\Core\Domain\Exception\IncompleteRecordException;
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
use TYPO3\CMS\Core\Domain\Record\LanguageInfo;
use TYPO3\CMS\Core\Domain\Record\SystemProperties;
use TYPO3\CMS\Core\Domain\Record\VersionInfo;
use TYPO3\CMS\Core\Schema\Capability\FieldCapability;
use TYPO3\CMS\Core\Schema\Capability\LanguageAwareSchemaCapability;
use TYPO3\CMS\Core\Schema\Capability\SystemInternalFieldCapability;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Creates record objects out of TCA-based database rows by evaluating the TCA columns and splitting
* everything which is not a declared column for a TCA type. This is usually the case when a TCA table
* has a 'typeField' defined, such as "pages", "be_users" and "tt_content".
*
* In addition, the RecordFactory can create "Resolved records" by utilizing the RecordFieldTransformer.
* A "Resolved record" is checked for the actual type (TCA field column type) and is then resolved to
* - a relation (records, files or folders - wrapped in collections)
* - an exploded list (e.g. static select)
* - a FlexForm field
* - a DateTime field.
*
* This means that the field value of a "Resolved Record" is expanded to the actual types (Date objects etc.)
*
* @internal not part of TYPO3 Core API yet.
*/
#[Autoconfigure(public: true)]
readonly class RecordFactory
{
public function __construct(
protected TcaSchemaFactory $schemaFactory,
protected RecordFieldTransformer $fieldTransformer,
protected EventDispatcherInterface $eventDispatcher,
) {}
/**
* Takes a full database record (the whole row), and creates a Record object out of it,
* based on the type of the record.
*
* This method does not handle special expansion of fields.
* @todo Now unused - we might want to remove this again
*/
public function createFromDatabaseRow(string $table, array $record): RecordInterface
{
$rawRecord = $this->createRawRecord($table, $record);
$schema = $this->schemaFactory->get($table);
$subSchema = null;
if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) {
$subSchema = $schema->getSubSchema($rawRecord->getRecordType());
}
// Only use the fields that are defined in the schema
$properties = [];
foreach ($record as $fieldName => $fieldValue) {
if ($subSchema) {
if (!$subSchema->hasField($fieldName)) {
continue;
}
$schema = $subSchema;
} elseif (!$schema->hasField($fieldName)) {
continue;
}
$properties[$fieldName] = $fieldValue;
}
return $this->createRecord($rawRecord, $properties, $schema);
}
/**
* Create a "resolved" record. Resolved means that the fields will have
* their values resolved and extended. A typical use-case is resolving
* of related records, or using \DateTimeImmutable objects for datetime fields.
*/
public function createResolvedRecordFromDatabaseRow(string $table, array $record, ?Context $context = null, ?RecordIdentityMap $recordIdentityMap = null): RecordInterface
{
$context = $context ?? GeneralUtility::makeInstance(Context::class);
/** @var RecordIdentityMap $recordIdentityMap */
$recordIdentityMap = $recordIdentityMap ?? GeneralUtility::makeInstance(RecordIdentityMap::class);
if ($recordIdentityMap->hasIdentifier($table, (int)($record['uid'] ?? 0))) {
return $recordIdentityMap->findByIdentifier($table, (int)$record['uid']);
}
$properties = [];
$rawRecord = $this->createRawRecord($table, $record);
$schema = $this->schemaFactory->get($table);
$subSchema = null;
if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) {
$subSchema = $schema->getSubSchema($rawRecord->getRecordType());
}
// Only use the fields that are defined in the schema
foreach ($record as $fieldName => $fieldValue) {
if ($subSchema) {
if (!$subSchema->hasField($fieldName)) {
continue;
}
$schema = $subSchema;
} elseif (!$schema->hasField($fieldName)) {
continue;
}
$fieldInformation = $schema->getField($fieldName);
$properties[$fieldName] = $this->fieldTransformer->transformField(
$fieldInformation,
$rawRecord,
$context,
$recordIdentityMap
);
}
$resolvedRecord = $this->createRecord($rawRecord, $properties, $schema, $context, $recordIdentityMap);
$recordIdentityMap->add($resolvedRecord);
return $resolvedRecord;
}
/**
* Creates a raw record object from a table and a record array.
*/
public function createRawRecord(string $table, array $record): RawRecord
{
if (!$this->schemaFactory->has($table)) {
throw new \InvalidArgumentException(
'Unable to create Record from non-TCA table "' . $table . '".',
1715266929
);
}
$schema = $this->schemaFactory->get($table);
$fullType = $table;
if ($schema->supportsSubSchema() && ($subSchemaTypeInformation = $schema->getSubSchemaTypeInformation())->isPointerToForeignFieldInForeignSchema() === false) {
// @todo Limitation to local SubSchemaDivisorField, because the actual record type is defined in foreign record.
$subSchemaDivisorFieldName = $subSchemaTypeInformation->getFieldName();
if (!isset($record[$subSchemaDivisorFieldName])) {
throw new \InvalidArgumentException(
'Missing typeField "' . $subSchemaDivisorFieldName . '" in record of requested table "' . $table . '".',
1715267513,
);
}
$recordType = (string)$record[$subSchemaDivisorFieldName];
$fullType .= '.' . $recordType;
}
$computedProperties = $this->extractComputedProperties($record);
// @todo We might want to throw an exception in case uid / pid are not defined.
return new RawRecord((int)($record['uid'] ?? 0), (int)($record['pid'] ?? 0), $record, $computedProperties, $fullType);
}
/**
* Quick helper function in order to avoid duplicate code.
*/
protected function createRecord(RawRecord $rawRecord, array $properties, TcaSchema $schema, ?Context $context = null, ?RecordIdentityMap $recordIdentityMap = null): RecordInterface
{
$context = $context ?? GeneralUtility::makeInstance(Context::class);
$mainSchema = $this->schemaFactory->get($rawRecord->getMainType());
$recordIdentityMap = $recordIdentityMap ?? GeneralUtility::makeInstance(RecordIdentityMap::class);
[$properties, $systemProperties] = $this->extractSystemInformation(
$mainSchema,
$rawRecord,
$properties,
);
$event = new RecordCreationEvent($properties, $rawRecord, $systemProperties, $context, $recordIdentityMap, $schema);
$this->eventDispatcher->dispatch($event);
if ($event->isPropagationStopped()) {
return $event->getRecord();
}
if ($event->getRawRecord()->getMainType() === 'pages') {
return new Page($event->getRawRecord(), $event->getProperties(), $event->getSystemProperties());
}
return new Record($event->getRawRecord(), $event->getProperties(), $event->getSystemProperties());
}
protected function extractComputedProperties(array &$record): ComputedProperties
{
$computed = $record['_computed'] ?? null;
if (is_array($computed)) {
$computedProperties = new ComputedProperties(
$computed['versionedUid'] ?? null,
$computed['localizedUid'] ?? null,
$computed['requestedOverlayLanguageId'] ?? null,
$computed['translationSource'] ?? null
);
unset($record['_computed']);
return $computedProperties;
}
$computedProperties = new ComputedProperties(
$record['_ORIG_uid'] ?? null,
$record['_LOCALIZED_UID'] ?? null,
$record['_REQUESTED_OVERLAY_LANGUAGE'] ?? null,
$record['_TRANSLATION_SOURCE'] ?? null
);
unset(
$record['_ORIG_uid'],
$record['_LOCALIZED_UID'],
$record['_REQUESTED_OVERLAY_LANGUAGE'],
$record['_TRANSLATION_SOURCE']
);
return $computedProperties;
}
protected function extractSystemInformation(TcaSchema $schema, RawRecord $rawRecord, array $properties): array
{
// Language information.
$systemProperties = [];
if ($schema->isLanguageAware()) {
/** @var LanguageAwareSchemaCapability $languageCapability */
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
$languageField = $languageCapability->getLanguageField()->getName();
$transOrigPointerField = $languageCapability->getTranslationOriginPointerField()->getName();
$translationSourceField = $languageCapability->getTranslationSourceField()?->getName() ?? '';
try {
$systemProperties['language'] = new LanguageInfo(
(int)$rawRecord->get($languageField),
(int)$rawRecord->get($transOrigPointerField),
$rawRecord->has($translationSourceField) ? (int)$rawRecord->get($translationSourceField) : null,
);
} catch (RecordPropertyNotFoundException $e) {
throw new IncompleteRecordException(
'Table "' . $schema->getName() . '" is defined as language aware but the record misses necessary fields: ' . $e->getMessage(),
1726046917
);
}
unset($properties[$languageField]);
unset($properties[$transOrigPointerField]);
if ($translationSourceField !== '') {
unset($properties[$translationSourceField]);
}
if ($languageCapability->hasDiffSourceField()) {
unset($properties[$languageCapability->getDiffSourceField()?->getName()]);
}
unset($properties['l10n_state']);
}
// Workspaces.
if ($schema->isWorkspaceAware()) {
try {
$systemProperties['version'] = new VersionInfo(
(int)$rawRecord->get('t3ver_wsid'),
(int)$rawRecord->get('t3ver_oid'),
VersionState::tryFrom((int)$rawRecord->get('t3ver_state')),
(int)$rawRecord->get('t3ver_stage'),
);
} catch (RecordPropertyNotFoundException $e) {
throw new IncompleteRecordException(
'Table "' . $schema->getName() . '" is defined as workspace aware but the record misses necessary fields: ' . $e->getMessage(),
1726046918
);
}
unset(
$properties['t3ver_wsid'],
$properties['t3ver_oid'],
$properties['t3ver_state'],
$properties['t3ver_stage']
);
}
// Date-related fields
foreach (TcaSchemaCapability::getSystemCapabilities() as $capability) {
if (!$schema->hasCapability($capability)) {
continue;
}
/** @var SystemInternalFieldCapability|FieldCapability $capabilityInstance */
$capabilityInstance = $schema->getCapability($capability);
$fieldName = $capabilityInstance->getFieldName();
if (!$rawRecord->has($fieldName)) {
throw new IncompleteRecordException(
'Table "' . $schema->getName() . '" has capability "' . $capability->name . '" set but the record misses the corresponding field "' . $fieldName . '"',
1726046919
);
}
switch ($capability) {
case TcaSchemaCapability::CreatedAt:
$systemProperties['createdAt'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::UpdatedAt:
$systemProperties['lastUpdatedAt'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::RestrictionStartTime:
$systemProperties['publishAt'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::RestrictionEndTime:
$systemProperties['publishUntil'] = DateTimeFactory::createFromTimestamp($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::SoftDelete:
$systemProperties['isDeleted'] = (bool)($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::EditLock:
$systemProperties['isLockedForEditing'] = (bool)($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::RestrictionDisabledField:
$systemProperties['isDisabled'] = (bool)($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::InternalDescription:
$systemProperties['description'] = $rawRecord->get($fieldName);
break;
case TcaSchemaCapability::SortByField:
$systemProperties['sorting'] = (int)($rawRecord->get($fieldName));
break;
case TcaSchemaCapability::RestrictionUserGroup:
$systemProperties['userGroupRestriction'] = GeneralUtility::intExplode(
',',
$rawRecord->get($fieldName),
true
);
break;
}
unset($properties[$fieldName]);
}
$systemProperties = new SystemProperties(
$systemProperties['language'] ?? null,
$systemProperties['version'] ?? null,
$systemProperties['isDeleted'] ?? null,
$systemProperties['isDisabled'] ?? null,
$systemProperties['isLockedForEditing'] ?? null,
$systemProperties['createdAt'] ?? null,
$systemProperties['lastUpdatedAt'] ?? null,
$systemProperties['publishAt'] ?? null,
$systemProperties['publishUntil'] ?? null,
$systemProperties['userGroupRestriction'] ?? null,
$systemProperties['sorting'] ?? null,
$systemProperties['description'] ?? null,
);
return [$properties, $systemProperties];
}
}
+53
View File
@@ -0,0 +1,53 @@
<?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\Core\Domain;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
/**
* An interface for database / TCA records.
*/
interface RecordInterface extends ContainerInterface
{
public function getUid(): int;
public function getPid(): int;
/**
* The full type contains the type of the record (e.g. "be_users", which is usually the TCA table)
* and the subtype of the record (such as "textpic" in tt_content records) separated by a ".".
*/
public function getFullType(): string;
/**
* The type contains the subtype of the record (such as "textpic"). Returns null if there is
* no "subtype".
*/
public function getRecordType(): ?string;
/**
* This is the TCA table for the record, all in lowercase.
*/
public function getMainType(): string;
public function toArray(): array;
public function getRawRecord(): ?RawRecord;
public function getComputedProperties(): ComputedProperties;
}
+35
View File
@@ -0,0 +1,35 @@
<?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\Core\Domain;
/**
* Used to initialize a record once a property is accessed
*
* @internal
*/
final readonly class RecordPropertyClosure
{
public function __construct(
private \Closure $instantiator
) {}
public function instantiate(): mixed
{
return ($this->instantiator)();
}
}
File diff suppressed because it is too large Load Diff