vendor/shopware/core/Framework/DataAbstractionLayer/EntityDefinition.php line 365

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer;
  3. use Shopware\Core\Content\Seo\SeoUrl\SeoUrlDefinition;
  4. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityHydrator;
  5. use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\EntityProtectionCollection;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Field\AutoIncrementField;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Field\CreatedAtField;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Computed;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Runtime;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\JsonField;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\LockedField;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Field\ParentAssociationField;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Field\UpdatedAtField;
  26. use Shopware\Core\Framework\Log\Package;
  27. use Shopware\Core\Framework\Struct\ArrayEntity;
  28. #[Package('core')]
  29. abstract class EntityDefinition
  30. {
  31. protected ?CompiledFieldCollection $fields = null;
  32. /**
  33. * @var EntityExtension[]
  34. */
  35. protected array $extensions = [];
  36. protected ?TranslationsAssociationField $translationField = null;
  37. protected ?CompiledFieldCollection $primaryKeys = null;
  38. protected DefinitionInstanceRegistry $registry;
  39. /**
  40. * @var TranslatedField[]
  41. */
  42. protected array $translatedFields = [];
  43. /**
  44. * @var Field[]
  45. */
  46. protected array $extensionFields = [];
  47. /**
  48. * @var EntityDefinition|false|null
  49. */
  50. private $parentDefinition = false;
  51. private string $className;
  52. private ?FieldVisibility $fieldVisibility = null;
  53. final public function __construct()
  54. {
  55. $this->className = static::class;
  56. }
  57. /**
  58. * @return class-string<EntityDefinition>
  59. */
  60. final public function getClass(): string
  61. {
  62. return static::class;
  63. }
  64. final public function isInstanceOf(EntityDefinition $other): bool
  65. {
  66. // same reference or instance of the other class
  67. return $this === $other
  68. || ($other->getClass() !== EntityDefinition::class && $this instanceof $other);
  69. }
  70. public function compile(DefinitionInstanceRegistry $registry): void
  71. {
  72. $this->registry = $registry;
  73. }
  74. final public function addExtension(EntityExtension $extension): void
  75. {
  76. $this->extensions[] = $extension;
  77. $this->fields = null;
  78. }
  79. /**
  80. * @internal
  81. * Use this only for test purposes
  82. */
  83. final public function removeExtension(EntityExtension $toDelete): void
  84. {
  85. foreach ($this->extensions as $key => $extension) {
  86. if (\get_class($extension) === \get_class($toDelete)) {
  87. unset($this->extensions[$key]);
  88. $this->fields = null;
  89. return;
  90. }
  91. }
  92. }
  93. abstract public function getEntityName(): string;
  94. final public function getFields(): CompiledFieldCollection
  95. {
  96. if ($this->fields !== null) {
  97. return $this->fields;
  98. }
  99. $fields = $this->defineFields();
  100. foreach ($this->defaultFields() as $field) {
  101. $fields->add($field);
  102. }
  103. foreach ($this->extensions as $extension) {
  104. $new = new FieldCollection();
  105. $extension->extendFields($new);
  106. foreach ($new as $field) {
  107. $field->addFlags(new Extension());
  108. if ($field instanceof AssociationField) {
  109. $fields->add($field);
  110. continue;
  111. }
  112. if ($field->is(Runtime::class)) {
  113. $fields->add($field);
  114. continue;
  115. }
  116. if ($field instanceof ReferenceVersionField) {
  117. $fields->add($field);
  118. continue;
  119. }
  120. if (!$field instanceof FkField) {
  121. throw new \Exception('Only AssociationFields, FkFields/ReferenceVersionFields for a ManyToOneAssociationField or fields flagged as Runtime can be added as Extension.');
  122. }
  123. if (!$this->hasAssociationWithStorageName($field->getStorageName(), $new)) {
  124. throw new \Exception(sprintf('FkField %s has no configured OneToOneAssociationField or ManyToOneAssociationField in entity %s', $field->getPropertyName(), $this->className));
  125. }
  126. $fields->add($field);
  127. }
  128. }
  129. foreach ($this->getBaseFields() as $baseField) {
  130. $fields->add($baseField);
  131. }
  132. foreach ($fields as $field) {
  133. if ($field instanceof TranslationsAssociationField) {
  134. $this->translationField = $field;
  135. $fields->add(
  136. (new JsonField('translated', 'translated'))->addFlags(new ApiAware(), new Computed(), new Runtime())
  137. );
  138. break;
  139. }
  140. }
  141. $this->fields = $fields->compile($this->registry);
  142. return $this->fields;
  143. }
  144. final public function getProtections(): EntityProtectionCollection
  145. {
  146. $protections = $this->defineProtections();
  147. foreach ($this->extensions as $extension) {
  148. if (!$extension instanceof EntityExtension) {
  149. continue;
  150. }
  151. $extension->extendProtections($protections);
  152. }
  153. return $protections;
  154. }
  155. final public function getField(string $propertyName): ?Field
  156. {
  157. return $this->getFields()->get($propertyName);
  158. }
  159. final public function getFieldVisibility(): FieldVisibility
  160. {
  161. if ($this->fieldVisibility) {
  162. return $this->fieldVisibility;
  163. }
  164. /** @var array<string> $internalProperties */
  165. $internalProperties = $this->getFields()
  166. ->fmap(function (Field $field): ?string {
  167. if ($field->is(ApiAware::class)) {
  168. return null;
  169. }
  170. return $field->getPropertyName();
  171. });
  172. return $this->fieldVisibility = new FieldVisibility(array_values($internalProperties));
  173. }
  174. /**
  175. * Phpstan will complain that we should specify the generic type if we hint that class strings
  176. * of EntityColllection should be returned.
  177. *
  178. * @return class-string
  179. */
  180. public function getCollectionClass(): string
  181. {
  182. return EntityCollection::class;
  183. }
  184. /**
  185. * @return class-string<Entity>
  186. */
  187. public function getEntityClass(): string
  188. {
  189. return ArrayEntity::class;
  190. }
  191. public function getParentDefinition(): ?EntityDefinition
  192. {
  193. if ($this->parentDefinition !== false) {
  194. return $this->parentDefinition;
  195. }
  196. $parentDefinitionClass = $this->getParentDefinitionClass();
  197. if ($parentDefinitionClass === null) {
  198. return $this->parentDefinition = null;
  199. }
  200. $this->parentDefinition = $this->registry->getByClassOrEntityName($parentDefinitionClass);
  201. return $this->parentDefinition;
  202. }
  203. final public function getTranslationDefinition(): ?EntityDefinition
  204. {
  205. // value is initialized from this method
  206. $this->getFields();
  207. if ($this->translationField === null) {
  208. return null;
  209. }
  210. return $this->translationField->getReferenceDefinition();
  211. }
  212. final public function getTranslationField(): ?TranslationsAssociationField
  213. {
  214. // value is initialized from this method
  215. $this->getFields();
  216. return $this->translationField;
  217. }
  218. final public function hasAutoIncrement(): bool
  219. {
  220. return $this->getField('autoIncrement') instanceof AutoIncrementField;
  221. }
  222. final public function getPrimaryKeys(): CompiledFieldCollection
  223. {
  224. if ($this->primaryKeys !== null) {
  225. return $this->primaryKeys;
  226. }
  227. $fields = $this->getFields()->filter(function (Field $field): bool {
  228. return $field->is(PrimaryKey::class);
  229. });
  230. $fields->sort(static function (Field $a, Field $b) {
  231. return $b->getExtractPriority() <=> $a->getExtractPriority();
  232. });
  233. return $this->primaryKeys = $fields;
  234. }
  235. /**
  236. * @return array<mixed>
  237. */
  238. public function getDefaults(): array
  239. {
  240. return [];
  241. }
  242. public function getChildDefaults(): array
  243. {
  244. return [];
  245. }
  246. public function isChildrenAware(): bool
  247. {
  248. //used in VersionManager
  249. return $this->getFields()->getChildrenAssociationField() !== null;
  250. }
  251. public function isParentAware(): bool
  252. {
  253. return $this->getFields()->get('parent') instanceof ParentAssociationField;
  254. }
  255. public function isInheritanceAware(): bool
  256. {
  257. return false;
  258. }
  259. public function isVersionAware(): bool
  260. {
  261. return $this->getFields()->has('versionId');
  262. }
  263. public function isLockAware(): bool
  264. {
  265. $field = $this->getFields()->get('locked');
  266. return $field && $field instanceof LockedField;
  267. }
  268. public function isSeoAware(): bool
  269. {
  270. $field = $this->getFields()->get('seoUrls');
  271. return $field instanceof OneToManyAssociationField && $field->getReferenceDefinition() instanceof SeoUrlDefinition;
  272. }
  273. public function since(): ?string
  274. {
  275. return null;
  276. }
  277. public function getHydratorClass(): string
  278. {
  279. return EntityHydrator::class;
  280. }
  281. /**
  282. * @internal
  283. *
  284. * @return mixed
  285. */
  286. public function decode(string $property, ?string $value)
  287. {
  288. $field = $this->getField($property);
  289. if ($field === null) {
  290. throw new \RuntimeException(sprintf('Field %s not found', $property));
  291. }
  292. return $field->getSerializer()->decode($field, $value);
  293. }
  294. public function getTranslatedFields(): array
  295. {
  296. return $this->getFields()->getTranslatedFields();
  297. }
  298. public function getExtensionFields(): array
  299. {
  300. return $this->getFields()->getExtensionFields();
  301. }
  302. protected function getParentDefinitionClass(): ?string
  303. {
  304. return null;
  305. }
  306. /**
  307. * @return Field[]
  308. */
  309. protected function defaultFields(): array
  310. {
  311. return [
  312. (new CreatedAtField())->addFlags(new ApiAware()),
  313. (new UpdatedAtField())->addFlags(new ApiAware()),
  314. ];
  315. }
  316. abstract protected function defineFields(): FieldCollection;
  317. protected function defineProtections(): EntityProtectionCollection
  318. {
  319. return new EntityProtectionCollection();
  320. }
  321. protected function getBaseFields(): array
  322. {
  323. return [];
  324. }
  325. private function hasAssociationWithStorageName(string $storageName, FieldCollection $new): bool
  326. {
  327. foreach ($new as $association) {
  328. if (!$association instanceof ManyToOneAssociationField && !$association instanceof OneToOneAssociationField) {
  329. continue;
  330. }
  331. if ($association->getStorageName() === $storageName) {
  332. return true;
  333. }
  334. }
  335. return false;
  336. }
  337. }