From 87d26e7eacf64833507c8a8d6485e290c553e7cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:08:31 +0000 Subject: [PATCH] Refactor Category, Footprint, and StorageLocation to use trait composition - Remove inheritance from AbstractPartsContainingDBElement - Add explicit trait usage: DBElementTrait, NamedElementTrait, TimestampTrait, AttachmentsTrait, MasterAttachmentTrait, StructuralElementTrait, ParametersTrait - Implement all required interfaces directly - Initialize traits in constructor - Add custom __clone and jsonSerialize methods Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com> --- src/Entity/Parts/Category.php | 63 ++++++++++++++++++++++++---- src/Entity/Parts/Footprint.php | 63 ++++++++++++++++++++++++---- src/Entity/Parts/StorageLocation.php | 62 ++++++++++++++++++++++++--- 3 files changed, 168 insertions(+), 20 deletions(-) diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 7fca81bc..2d368d95 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -39,28 +39,44 @@ use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Entity\Base\AttachmentsTrait; +use App\Entity\Base\DBElementTrait; +use App\Entity\Base\MasterAttachmentTrait; +use App\Entity\Base\NamedElementTrait; +use App\Entity\Base\StructuralElementTrait; +use App\Entity\Base\TimestampTrait; +use App\Entity\Contracts\DBElementInterface; +use App\Entity\Contracts\HasAttachmentsInterface; +use App\Entity\Contracts\HasMasterAttachmentInterface; +use App\Entity\Contracts\HasParametersInterface; +use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Contracts\StructuralElementInterface; +use App\Entity\Contracts\TimeStampableInterface; use App\Entity\EDA\EDACategoryInfo; +use App\Entity\Parameters\ParametersTrait; +use App\EntityListeners\TreeCacheInvalidationListener; use App\Repository\Parts\CategoryRepository; +use App\Validator\Constraints\UniqueObjectCollection; use Doctrine\DBAL\Types\Types; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\CategoryAttachment; -use App\Entity\Base\AbstractPartsContainingDBElement; -use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\CategoryParameter; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** * This entity describes a category, a part can belong to, which is used to group parts by their function. - * - * @extends AbstractPartsContainingDBElement */ #[ORM\Entity(repositoryClass: CategoryRepository::class)] #[ORM\Table(name: '`categories`')] #[ORM\Index(columns: ['name'], name: 'category_idx_name')] #[ORM\Index(columns: ['parent_id', 'name'], name: 'category_idx_parent_name')] +#[ORM\HasLifecycleCallbacks] +#[ORM\EntityListeners([TreeCacheInvalidationListener::class])] +#[UniqueEntity(fields: ['name', 'parent'], message: 'structural.entity.unique_name', ignoreNull: false)] #[ApiResource( operations: [ new Get(security: 'is_granted("read", object)'), @@ -89,8 +105,16 @@ use Symfony\Component\Validator\Constraints as Assert; #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] -class Category extends AbstractPartsContainingDBElement +class Category implements DBElementInterface, NamedElementInterface, TimeStampableInterface, HasAttachmentsInterface, HasMasterAttachmentInterface, StructuralElementInterface, HasParametersInterface, \Stringable, \JsonSerializable { + use DBElementTrait; + use NamedElementTrait; + use TimestampTrait; + use AttachmentsTrait; + use MasterAttachmentTrait; + use StructuralElementTrait; + use ParametersTrait; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] #[ORM\OrderBy(['name' => Criteria::ASC])] protected Collection $children; @@ -99,7 +123,7 @@ class Category extends AbstractPartsContainingDBElement #[ORM\JoinColumn(name: 'parent_id')] #[Groups(['category:read', 'category:write'])] #[ApiProperty(readableLink: false, writableLink: false)] - protected ?AbstractStructuralDBElement $parent = null; + protected ?self $parent = null; #[Groups(['category:read', 'category:write'])] protected string $comment = ''; @@ -184,6 +208,7 @@ class Category extends AbstractPartsContainingDBElement /** @var Collection */ #[Assert\Valid] + #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] #[Groups(['full', 'category:read', 'category:write'])] #[ORM\OneToMany(mappedBy: 'element', targetEntity: CategoryParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] @@ -201,13 +226,37 @@ class Category extends AbstractPartsContainingDBElement public function __construct() { - parent::__construct(); + $this->initializeAttachments(); + $this->initializeStructuralElement(); $this->children = new ArrayCollection(); $this->attachments = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->eda_info = new EDACategoryInfo(); } + public function __clone() + { + if ($this->id) { + $this->cloneDBElement(); + $this->cloneAttachments(); + + // We create a new object, so give it a new creation date + $this->addedDate = null; + + //Deep clone parameters + $parameters = $this->parameters; + $this->parameters = new ArrayCollection(); + foreach ($parameters as $parameter) { + $this->addParameter(clone $parameter); + } + } + } + + public function jsonSerialize(): array + { + return ['@id' => $this->getID()]; + } + public function getPartnameHint(): string { return $this->partname_hint; diff --git a/src/Entity/Parts/Footprint.php b/src/Entity/Parts/Footprint.php index 6b043562..95d51684 100644 --- a/src/Entity/Parts/Footprint.php +++ b/src/Entity/Parts/Footprint.php @@ -39,27 +39,43 @@ use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Entity\Base\AttachmentsTrait; +use App\Entity\Base\DBElementTrait; +use App\Entity\Base\MasterAttachmentTrait; +use App\Entity\Base\NamedElementTrait; +use App\Entity\Base\StructuralElementTrait; +use App\Entity\Base\TimestampTrait; +use App\Entity\Contracts\DBElementInterface; +use App\Entity\Contracts\HasAttachmentsInterface; +use App\Entity\Contracts\HasMasterAttachmentInterface; +use App\Entity\Contracts\HasParametersInterface; +use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Contracts\StructuralElementInterface; +use App\Entity\Contracts\TimeStampableInterface; use App\Entity\EDA\EDAFootprintInfo; +use App\Entity\Parameters\ParametersTrait; +use App\EntityListeners\TreeCacheInvalidationListener; use App\Repository\Parts\FootprintRepository; -use App\Entity\Base\AbstractStructuralDBElement; +use App\Validator\Constraints\UniqueObjectCollection; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\FootprintAttachment; -use App\Entity\Base\AbstractPartsContainingDBElement; use App\Entity\Parameters\FootprintParameter; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** * This entity represents a footprint of a part (its physical dimensions and shape). - * - * @extends AbstractPartsContainingDBElement */ #[ORM\Entity(repositoryClass: FootprintRepository::class)] #[ORM\Table('`footprints`')] #[ORM\Index(columns: ['name'], name: 'footprint_idx_name')] #[ORM\Index(columns: ['parent_id', 'name'], name: 'footprint_idx_parent_name')] +#[ORM\HasLifecycleCallbacks] +#[ORM\EntityListeners([TreeCacheInvalidationListener::class])] +#[UniqueEntity(fields: ['name', 'parent'], message: 'structural.entity.unique_name', ignoreNull: false)] #[ApiResource( operations: [ new Get(security: 'is_granted("read", object)'), @@ -88,13 +104,21 @@ use Symfony\Component\Validator\Constraints as Assert; #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] -class Footprint extends AbstractPartsContainingDBElement +class Footprint implements DBElementInterface, NamedElementInterface, TimeStampableInterface, HasAttachmentsInterface, HasMasterAttachmentInterface, StructuralElementInterface, HasParametersInterface, \Stringable, \JsonSerializable { + use DBElementTrait; + use NamedElementTrait; + use TimestampTrait; + use AttachmentsTrait; + use MasterAttachmentTrait; + use StructuralElementTrait; + use ParametersTrait; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] #[ORM\JoinColumn(name: 'parent_id')] #[Groups(['footprint:read', 'footprint:write'])] #[ApiProperty(readableLink: false, writableLink: false)] - protected ?AbstractStructuralDBElement $parent = null; + protected ?self $parent = null; #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] #[ORM\OrderBy(['name' => Criteria::ASC])] @@ -128,6 +152,7 @@ class Footprint extends AbstractPartsContainingDBElement /** @var Collection */ #[Assert\Valid] + #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] #[ORM\OneToMany(mappedBy: 'element', targetEntity: FootprintParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] #[Groups(['footprint:read', 'footprint:write'])] @@ -145,13 +170,37 @@ class Footprint extends AbstractPartsContainingDBElement public function __construct() { - parent::__construct(); + $this->initializeAttachments(); + $this->initializeStructuralElement(); $this->children = new ArrayCollection(); $this->attachments = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->eda_info = new EDAFootprintInfo(); } + public function __clone() + { + if ($this->id) { + $this->cloneDBElement(); + $this->cloneAttachments(); + + // We create a new object, so give it a new creation date + $this->addedDate = null; + + //Deep clone parameters + $parameters = $this->parameters; + $this->parameters = new ArrayCollection(); + foreach ($parameters as $parameter) { + $this->addParameter(clone $parameter); + } + } + } + + public function jsonSerialize(): array + { + return ['@id' => $this->getID()]; + } + /**************************************** * Getters ****************************************/ diff --git a/src/Entity/Parts/StorageLocation.php b/src/Entity/Parts/StorageLocation.php index 6c455ae5..1fd225ae 100644 --- a/src/Entity/Parts/StorageLocation.php +++ b/src/Entity/Parts/StorageLocation.php @@ -39,27 +39,44 @@ use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Entity\Base\AttachmentsTrait; +use App\Entity\Base\DBElementTrait; +use App\Entity\Base\MasterAttachmentTrait; +use App\Entity\Base\NamedElementTrait; +use App\Entity\Base\StructuralElementTrait; +use App\Entity\Base\TimestampTrait; +use App\Entity\Contracts\DBElementInterface; +use App\Entity\Contracts\HasAttachmentsInterface; +use App\Entity\Contracts\HasMasterAttachmentInterface; +use App\Entity\Contracts\HasParametersInterface; +use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Contracts\StructuralElementInterface; +use App\Entity\Contracts\TimeStampableInterface; +use App\Entity\Parameters\ParametersTrait; +use App\EntityListeners\TreeCacheInvalidationListener; use App\Repository\Parts\StorelocationRepository; +use App\Validator\Constraints\UniqueObjectCollection; use Doctrine\DBAL\Types\Types; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\StorageLocationAttachment; -use App\Entity\Base\AbstractPartsContainingDBElement; -use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\StorageLocationParameter; use App\Entity\UserSystem\User; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** * This entity represents a storage location, where parts can be stored. - * @extends AbstractPartsContainingDBElement */ #[ORM\Entity(repositoryClass: StorelocationRepository::class)] #[ORM\Table('`storelocations`')] #[ORM\Index(columns: ['name'], name: 'location_idx_name')] #[ORM\Index(columns: ['parent_id', 'name'], name: 'location_idx_parent_name')] +#[ORM\HasLifecycleCallbacks] +#[ORM\EntityListeners([TreeCacheInvalidationListener::class])] +#[UniqueEntity(fields: ['name', 'parent'], message: 'structural.entity.unique_name', ignoreNull: false)] #[ApiResource( operations: [ new Get(security: 'is_granted("read", object)'), @@ -88,8 +105,16 @@ use Symfony\Component\Validator\Constraints as Assert; #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] -class StorageLocation extends AbstractPartsContainingDBElement +class StorageLocation implements DBElementInterface, NamedElementInterface, TimeStampableInterface, HasAttachmentsInterface, HasMasterAttachmentInterface, StructuralElementInterface, HasParametersInterface, \Stringable, \JsonSerializable { + use DBElementTrait; + use NamedElementTrait; + use TimestampTrait; + use AttachmentsTrait; + use MasterAttachmentTrait; + use StructuralElementTrait; + use ParametersTrait; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] #[ORM\OrderBy(['name' => Criteria::ASC])] protected Collection $children; @@ -98,7 +123,7 @@ class StorageLocation extends AbstractPartsContainingDBElement #[ORM\JoinColumn(name: 'parent_id')] #[Groups(['location:read', 'location:write'])] #[ApiProperty(readableLink: false, writableLink: false)] - protected ?AbstractStructuralDBElement $parent = null; + protected ?self $parent = null; #[Groups(['location:read', 'location:write'])] protected string $comment = ''; @@ -114,6 +139,7 @@ class StorageLocation extends AbstractPartsContainingDBElement /** @var Collection */ #[Assert\Valid] + #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] #[ORM\OneToMany(mappedBy: 'element', targetEntity: StorageLocationParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] #[Groups(['location:read', 'location:write'])] @@ -295,9 +321,33 @@ class StorageLocation extends AbstractPartsContainingDBElement } public function __construct() { - parent::__construct(); + $this->initializeAttachments(); + $this->initializeStructuralElement(); $this->children = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->attachments = new ArrayCollection(); } + + public function __clone() + { + if ($this->id) { + $this->cloneDBElement(); + $this->cloneAttachments(); + + // We create a new object, so give it a new creation date + $this->addedDate = null; + + //Deep clone parameters + $parameters = $this->parameters; + $this->parameters = new ArrayCollection(); + foreach ($parameters as $parameter) { + $this->addParameter(clone $parameter); + } + } + } + + public function jsonSerialize(): array + { + return ['@id' => $this->getID()]; + } }