<?php declare(strict_types=1);
namespace Shopware\Core\Framework\DataAbstractionLayer;
use Shopware\Core\Content\Seo\SeoUrl\SeoUrlDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityHydrator;
use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\EntityProtectionCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\AutoIncrementField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\CreatedAtField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Computed;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Runtime;
use Shopware\Core\Framework\DataAbstractionLayer\Field\JsonField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\LockedField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ParentAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\UpdatedAtField;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Struct\ArrayEntity;
#[Package('core')]
abstract class EntityDefinition
{
protected ?CompiledFieldCollection $fields = null;
/**
* @var EntityExtension[]
*/
protected array $extensions = [];
protected ?TranslationsAssociationField $translationField = null;
protected ?CompiledFieldCollection $primaryKeys = null;
protected DefinitionInstanceRegistry $registry;
/**
* @var TranslatedField[]
*/
protected array $translatedFields = [];
/**
* @var Field[]
*/
protected array $extensionFields = [];
/**
* @var EntityDefinition|false|null
*/
private $parentDefinition = false;
private string $className;
private ?FieldVisibility $fieldVisibility = null;
final public function __construct()
{
$this->className = static::class;
}
/**
* @return class-string<EntityDefinition>
*/
final public function getClass(): string
{
return static::class;
}
final public function isInstanceOf(EntityDefinition $other): bool
{
// same reference or instance of the other class
return $this === $other
|| ($other->getClass() !== EntityDefinition::class && $this instanceof $other);
}
public function compile(DefinitionInstanceRegistry $registry): void
{
$this->registry = $registry;
}
final public function addExtension(EntityExtension $extension): void
{
$this->extensions[] = $extension;
$this->fields = null;
}
/**
* @internal
* Use this only for test purposes
*/
final public function removeExtension(EntityExtension $toDelete): void
{
foreach ($this->extensions as $key => $extension) {
if (\get_class($extension) === \get_class($toDelete)) {
unset($this->extensions[$key]);
$this->fields = null;
return;
}
}
}
abstract public function getEntityName(): string;
final public function getFields(): CompiledFieldCollection
{
if ($this->fields !== null) {
return $this->fields;
}
$fields = $this->defineFields();
foreach ($this->defaultFields() as $field) {
$fields->add($field);
}
foreach ($this->extensions as $extension) {
$new = new FieldCollection();
$extension->extendFields($new);
foreach ($new as $field) {
$field->addFlags(new Extension());
if ($field instanceof AssociationField) {
$fields->add($field);
continue;
}
if ($field->is(Runtime::class)) {
$fields->add($field);
continue;
}
if ($field instanceof ReferenceVersionField) {
$fields->add($field);
continue;
}
if (!$field instanceof FkField) {
throw new \Exception('Only AssociationFields, FkFields/ReferenceVersionFields for a ManyToOneAssociationField or fields flagged as Runtime can be added as Extension.');
}
if (!$this->hasAssociationWithStorageName($field->getStorageName(), $new)) {
throw new \Exception(sprintf('FkField %s has no configured OneToOneAssociationField or ManyToOneAssociationField in entity %s', $field->getPropertyName(), $this->className));
}
$fields->add($field);
}
}
foreach ($this->getBaseFields() as $baseField) {
$fields->add($baseField);
}
foreach ($fields as $field) {
if ($field instanceof TranslationsAssociationField) {
$this->translationField = $field;
$fields->add(
(new JsonField('translated', 'translated'))->addFlags(new ApiAware(), new Computed(), new Runtime())
);
break;
}
}
$this->fields = $fields->compile($this->registry);
return $this->fields;
}
final public function getProtections(): EntityProtectionCollection
{
$protections = $this->defineProtections();
foreach ($this->extensions as $extension) {
if (!$extension instanceof EntityExtension) {
continue;
}
$extension->extendProtections($protections);
}
return $protections;
}
final public function getField(string $propertyName): ?Field
{
return $this->getFields()->get($propertyName);
}
final public function getFieldVisibility(): FieldVisibility
{
if ($this->fieldVisibility) {
return $this->fieldVisibility;
}
/** @var array<string> $internalProperties */
$internalProperties = $this->getFields()
->fmap(function (Field $field): ?string {
if ($field->is(ApiAware::class)) {
return null;
}
return $field->getPropertyName();
});
return $this->fieldVisibility = new FieldVisibility(array_values($internalProperties));
}
/**
* Phpstan will complain that we should specify the generic type if we hint that class strings
* of EntityColllection should be returned.
*
* @return class-string
*/
public function getCollectionClass(): string
{
return EntityCollection::class;
}
/**
* @return class-string<Entity>
*/
public function getEntityClass(): string
{
return ArrayEntity::class;
}
public function getParentDefinition(): ?EntityDefinition
{
if ($this->parentDefinition !== false) {
return $this->parentDefinition;
}
$parentDefinitionClass = $this->getParentDefinitionClass();
if ($parentDefinitionClass === null) {
return $this->parentDefinition = null;
}
$this->parentDefinition = $this->registry->getByClassOrEntityName($parentDefinitionClass);
return $this->parentDefinition;
}
final public function getTranslationDefinition(): ?EntityDefinition
{
// value is initialized from this method
$this->getFields();
if ($this->translationField === null) {
return null;
}
return $this->translationField->getReferenceDefinition();
}
final public function getTranslationField(): ?TranslationsAssociationField
{
// value is initialized from this method
$this->getFields();
return $this->translationField;
}
final public function hasAutoIncrement(): bool
{
return $this->getField('autoIncrement') instanceof AutoIncrementField;
}
final public function getPrimaryKeys(): CompiledFieldCollection
{
if ($this->primaryKeys !== null) {
return $this->primaryKeys;
}
$fields = $this->getFields()->filter(function (Field $field): bool {
return $field->is(PrimaryKey::class);
});
$fields->sort(static function (Field $a, Field $b) {
return $b->getExtractPriority() <=> $a->getExtractPriority();
});
return $this->primaryKeys = $fields;
}
/**
* @return array<mixed>
*/
public function getDefaults(): array
{
return [];
}
public function getChildDefaults(): array
{
return [];
}
public function isChildrenAware(): bool
{
//used in VersionManager
return $this->getFields()->getChildrenAssociationField() !== null;
}
public function isParentAware(): bool
{
return $this->getFields()->get('parent') instanceof ParentAssociationField;
}
public function isInheritanceAware(): bool
{
return false;
}
public function isVersionAware(): bool
{
return $this->getFields()->has('versionId');
}
public function isLockAware(): bool
{
$field = $this->getFields()->get('locked');
return $field && $field instanceof LockedField;
}
public function isSeoAware(): bool
{
$field = $this->getFields()->get('seoUrls');
return $field instanceof OneToManyAssociationField && $field->getReferenceDefinition() instanceof SeoUrlDefinition;
}
public function since(): ?string
{
return null;
}
public function getHydratorClass(): string
{
return EntityHydrator::class;
}
/**
* @internal
*
* @return mixed
*/
public function decode(string $property, ?string $value)
{
$field = $this->getField($property);
if ($field === null) {
throw new \RuntimeException(sprintf('Field %s not found', $property));
}
return $field->getSerializer()->decode($field, $value);
}
public function getTranslatedFields(): array
{
return $this->getFields()->getTranslatedFields();
}
public function getExtensionFields(): array
{
return $this->getFields()->getExtensionFields();
}
protected function getParentDefinitionClass(): ?string
{
return null;
}
/**
* @return Field[]
*/
protected function defaultFields(): array
{
return [
(new CreatedAtField())->addFlags(new ApiAware()),
(new UpdatedAtField())->addFlags(new ApiAware()),
];
}
abstract protected function defineFields(): FieldCollection;
protected function defineProtections(): EntityProtectionCollection
{
return new EntityProtectionCollection();
}
protected function getBaseFields(): array
{
return [];
}
private function hasAssociationWithStorageName(string $storageName, FieldCollection $new): bool
{
foreach ($new as $association) {
if (!$association instanceof ManyToOneAssociationField && !$association instanceof OneToOneAssociationField) {
continue;
}
if ($association->getStorageName() === $storageName) {
return true;
}
}
return false;
}
}