diff --git a/src/Entity/Attachments/AttachmentContainingDBElement.php b/src/Entity/Attachments/AttachmentContainingDBElement.php index a78cb1f4..0efd1022 100644 --- a/src/Entity/Attachments/AttachmentContainingDBElement.php +++ b/src/Entity/Attachments/AttachmentContainingDBElement.php @@ -24,13 +24,11 @@ namespace App\Entity\Attachments; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\MasterAttachmentTrait; +use App\Entity\Base\AttachmentsTrait; use App\Entity\Contracts\HasAttachmentsInterface; use App\Entity\Contracts\HasMasterAttachmentInterface; use App\Repository\AttachmentContainingDBElementRepository; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; /** * @template AT of Attachment @@ -39,83 +37,18 @@ use Symfony\Component\Serializer\Annotation\Groups; abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface { use MasterAttachmentTrait; - - /** - * @var Collection - * @phpstan-var Collection - * ORM Mapping is done in subclasses (e.g. Part) - */ - #[Groups(['full', 'import'])] - protected Collection $attachments; + use AttachmentsTrait; public function __construct() { - $this->attachments = new ArrayCollection(); + $this->initializeAttachments(); } public function __clone() { - if ($this->id) { - $attachments = $this->attachments; - $this->attachments = new ArrayCollection(); - //Set master attachment is needed - foreach ($attachments as $attachment) { - $clone = clone $attachment; - if ($attachment === $this->master_picture_attachment) { - $this->setMasterPictureAttachment($clone); - } - $this->addAttachment($clone); - } - } + $this->cloneAttachments(); //Parent has to be last call, as it resets the ID parent::__clone(); } - - /******************************************************************************** - * - * Getters - * - *********************************************************************************/ - - /** - * Gets all attachments associated with this element. - */ - public function getAttachments(): Collection - { - return $this->attachments; - } - - /** - * Adds an attachment to this element. - * - * @param Attachment $attachment Attachment - * - * @return $this - */ - public function addAttachment(Attachment $attachment): self - { - //Attachment must be associated with this element - $attachment->setElement($this); - $this->attachments->add($attachment); - - return $this; - } - - /** - * Removes the given attachment from this element. - * - * @return $this - */ - public function removeAttachment(Attachment $attachment): self - { - $this->attachments->removeElement($attachment); - - //Check if this is the master attachment -> remove it from master attachment too, or it can not be deleted from DB... - if ($attachment === $this->getMasterPictureAttachment()) { - $this->setMasterPictureAttachment(null); - } - - return $this; - } } diff --git a/src/Entity/Base/AbstractCompany.php b/src/Entity/Base/AbstractCompany.php index 7d05c93f..81917da6 100644 --- a/src/Entity/Base/AbstractCompany.php +++ b/src/Entity/Base/AbstractCompany.php @@ -24,11 +24,9 @@ namespace App\Entity\Base; use App\Entity\Attachments\Attachment; use App\Entity\Parameters\AbstractParameter; -use Doctrine\DBAL\Types\Types; +use App\Entity\Contracts\CompanyInterface; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; -use function is_string; -use Symfony\Component\Validator\Constraints as Assert; /** * This abstract class is used for companies like suppliers or manufacturers. @@ -38,226 +36,15 @@ use Symfony\Component\Validator\Constraints as Assert; * @extends AbstractPartsContainingDBElement */ #[ORM\MappedSuperclass] -abstract class AbstractCompany extends AbstractPartsContainingDBElement +abstract class AbstractCompany extends AbstractPartsContainingDBElement implements CompanyInterface { + use CompanyTrait; + #[Groups(['company:read'])] protected ?\DateTimeImmutable $addedDate = null; #[Groups(['company:read'])] protected ?\DateTimeImmutable $lastModified = null; - /** - * @var string The address of the company - */ - #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] - #[ORM\Column(type: Types::STRING)] - #[Assert\Length(max: 255)] - protected string $address = ''; - - /** - * @var string The phone number of the company - */ - #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] - #[ORM\Column(type: Types::STRING)] - #[Assert\Length(max: 255)] - protected string $phone_number = ''; - - /** - * @var string The fax number of the company - */ - #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] - #[ORM\Column(type: Types::STRING)] - #[Assert\Length(max: 255)] - protected string $fax_number = ''; - - /** - * @var string The email address of the company - */ - #[Assert\Email] - #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] - #[ORM\Column(type: Types::STRING)] - #[Assert\Length(max: 255)] - protected string $email_address = ''; - - /** - * @var string The website of the company - */ - #[Assert\Url(requireTld: false)] - #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] - #[ORM\Column(type: Types::STRING, length: 2048)] - #[Assert\Length(max: 2048)] - protected string $website = ''; - #[Groups(['company:read', 'company:write', 'import', 'full', 'extended'])] protected string $comment = ''; - - /** - * @var string The link to the website of an article. Use %PARTNUMBER% as placeholder for the part number. - */ - #[ORM\Column(type: Types::STRING, length: 2048)] - #[Assert\Length(max: 2048)] - #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] - protected string $auto_product_url = ''; - - /******************************************************************************** - * - * Getters - * - *********************************************************************************/ - - /** - * Get the address. - * - * @return string the address of the company (with "\n" as line break) - */ - public function getAddress(): string - { - return $this->address; - } - - /** - * Get the phone number. - * - * @return string the phone number of the company - */ - public function getPhoneNumber(): string - { - return $this->phone_number; - } - - /** - * Get the fax number. - * - * @return string the fax number of the company - */ - public function getFaxNumber(): string - { - return $this->fax_number; - } - - /** - * Get the e-mail address. - * - * @return string the e-mail address of the company - */ - public function getEmailAddress(): string - { - return $this->email_address; - } - - /** - * Get the website. - * - * @return string the website of the company - */ - public function getWebsite(): string - { - return $this->website; - } - - /** - * Get the link to the website of an article. - * - * @param string|null $partnr * NULL for returning the URL with a placeholder for the part number - * * or the part number for returning the direct URL to the article - * - * @return string the link to the article - */ - public function getAutoProductUrl(?string $partnr = null): string - { - if (is_string($partnr)) { - return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url); - } - - return $this->auto_product_url; - } - - /******************************************************************************** - * - * Setters - * - *********************************************************************************/ - - /** - * Set the addres. - * - * @param string $new_address the new address (with "\n" as line break) - * - * @return $this - */ - public function setAddress(string $new_address): self - { - $this->address = $new_address; - - return $this; - } - - /** - * Set the phone number. - * - * @param string $new_phone_number the new phone number - * - * @return $this - */ - public function setPhoneNumber(string $new_phone_number): self - { - $this->phone_number = $new_phone_number; - - return $this; - } - - /** - * Set the fax number. - * - * @param string $new_fax_number the new fax number - * - * @return $this - */ - public function setFaxNumber(string $new_fax_number): self - { - $this->fax_number = $new_fax_number; - - return $this; - } - - /** - * Set the e-mail address. - * - * @param string $new_email_address the new e-mail address - * - * @return $this - */ - public function setEmailAddress(string $new_email_address): self - { - $this->email_address = $new_email_address; - - return $this; - } - - /** - * Set the website. - * - * @param string $new_website the new website - * - * @return $this - */ - public function setWebsite(string $new_website): self - { - $this->website = $new_website; - - return $this; - } - - /** - * Set the link to the website of an article. - * - * @param string $new_url the new URL with the placeholder %PARTNUMBER% for the part number - * - * @return $this - */ - public function setAutoProductUrl(string $new_url): self - { - $this->auto_product_url = $new_url; - - return $this; - } } diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index a088b3df..b4083b24 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -38,6 +38,7 @@ use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; +use App\Entity\Contracts\DBElementInterface; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Category; use App\Entity\PriceInformations\Pricedetail; @@ -56,11 +57,9 @@ use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Supplier; use App\Entity\UserSystem\User; use App\Repository\DBElementRepository; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JsonSerializable; use Symfony\Component\Serializer\Annotation\DiscriminatorMap; -use Symfony\Component\Serializer\Annotation\Groups; /** * This class is for managing all database objects. @@ -106,36 +105,13 @@ use Symfony\Component\Serializer\Annotation\Groups; 'user' => User::class] )] #[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)] -abstract class AbstractDBElement implements JsonSerializable +abstract class AbstractDBElement implements JsonSerializable, DBElementInterface { - /** @var int|null The Identification number for this part. This value is unique for the element in this table. - * Null if the element is not saved to DB yet. - */ - #[Groups(['full', 'api:basic:read'])] - #[ORM\Column(type: Types::INTEGER)] - #[ORM\Id] - #[ORM\GeneratedValue] - protected ?int $id = null; + use DBElementTrait; public function __clone() { - if ($this->id) { - //Set ID to null, so that an new entry is created - $this->id = null; - } - } - - /** - * Get the ID. The ID can be zero, or even negative (for virtual elements). If an element is virtual, can be - * checked with isVirtualElement(). - * - * Returns null, if the element is not saved to the DB yet. - * - * @return int|null the ID of this element - */ - public function getID(): ?int - { - return $this->id; + $this->cloneDBElement(); } public function jsonSerialize(): array diff --git a/src/Entity/Base/AbstractNamedDBElement.php b/src/Entity/Base/AbstractNamedDBElement.php index f7939589..5b654d23 100644 --- a/src/Entity/Base/AbstractNamedDBElement.php +++ b/src/Entity/Base/AbstractNamedDBElement.php @@ -23,12 +23,9 @@ declare(strict_types=1); namespace App\Entity\Base; use App\Repository\NamedDBElementRepository; -use Doctrine\DBAL\Types\Types; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Contracts\TimeStampableInterface; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; -use Symfony\Component\Validator\Constraints as Assert; /** * All subclasses of this class have an attribute "name". @@ -38,26 +35,7 @@ use Symfony\Component\Validator\Constraints as Assert; abstract class AbstractNamedDBElement extends AbstractDBElement implements NamedElementInterface, TimeStampableInterface, \Stringable { use TimestampTrait; - - /** - * @var string The name of this element - */ - #[Assert\NotBlank] - #[Groups(['simple', 'extended', 'full', 'import', 'api:basic:read', 'api:basic:write'])] - #[ORM\Column(type: Types::STRING)] - #[Assert\Length(max: 255)] - protected string $name = ''; - - /****************************************************************************** - * - * Helpers - * - ******************************************************************************/ - - public function __toString(): string - { - return $this->getName(); - } + use NamedElementTrait; public function __clone() { @@ -65,40 +43,6 @@ abstract class AbstractNamedDBElement extends AbstractDBElement implements Named //We create a new object, so give it a new creation date $this->addedDate = null; } - parent::__clone(); // TODO: Change the autogenerated stub - } - - /******************************************************************************** - * - * Getters - * - *********************************************************************************/ - - /** - * Get the name of this element. - * - * @return string the name of this element - */ - public function getName(): string - { - return $this->name; - } - - /******************************************************************************** - * - * Setters - * - *********************************************************************************/ - - /** - * Change the name of this element. - * - * @param string $new_name the new name - */ - public function setName(string $new_name): self - { - $this->name = $new_name; - - return $this; + parent::__clone(); } } diff --git a/src/Entity/Base/AbstractStructuralDBElement.php b/src/Entity/Base/AbstractStructuralDBElement.php index 660710db..1d599ea7 100644 --- a/src/Entity/Base/AbstractStructuralDBElement.php +++ b/src/Entity/Base/AbstractStructuralDBElement.php @@ -24,22 +24,18 @@ namespace App\Entity\Base; use App\Entity\Attachments\Attachment; use App\Entity\Parameters\AbstractParameter; +use App\Entity\Contracts\StructuralElementInterface; +use App\Entity\Contracts\HasParametersInterface; use App\Repository\StructuralDBElementRepository; use App\EntityListeners\TreeCacheInvalidationListener; use App\Validator\Constraints\UniqueObjectCollection; -use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Parameters\ParametersTrait; -use App\Validator\Constraints\NoneOfItsChildren; -use Symfony\Component\Serializer\Annotation\SerializedName; -use Symfony\Component\Validator\Constraints as Assert; -use function count; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use InvalidArgumentException; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; /** * All elements with the fields "id", "name" and "parent_id" (at least). @@ -62,52 +58,10 @@ use Symfony\Component\Serializer\Annotation\Groups; #[UniqueEntity(fields: ['name', 'parent'], message: 'structural.entity.unique_name', ignoreNull: false)] #[ORM\MappedSuperclass(repositoryClass: StructuralDBElementRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] -abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement +abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement implements StructuralElementInterface, HasParametersInterface { use ParametersTrait; - - /** - * This is a not standard character, so build a const, so a dev can easily use it. - */ - final public const PATH_DELIMITER_ARROW = ' → '; - - /** - * @var string The comment info for this element as markdown - */ - #[Groups(['full', 'import'])] - #[ORM\Column(type: Types::TEXT)] - protected string $comment = ''; - - /** - * @var bool If this property is set, this element can not be selected for part properties. - * Useful if this element should be used only for grouping, sorting. - */ - #[Groups(['full', 'import'])] - #[ORM\Column(type: Types::BOOLEAN)] - protected bool $not_selectable = false; - - /** - * @var int - */ - protected int $level = 0; - - /** - * We can not define the mapping here, or we will get an exception. Unfortunately we have to do the mapping in the - * subclasses. - * - * @var Collection - * @phpstan-var Collection - */ - #[Groups(['include_children'])] - protected Collection $children; - - /** - * @var AbstractStructuralDBElement|null - * @phpstan-var static|null - */ - #[Groups(['include_parents', 'import'])] - #[NoneOfItsChildren] - protected ?AbstractStructuralDBElement $parent = null; + use StructuralElementTrait; /** * Mapping done in subclasses. @@ -119,21 +73,10 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] protected Collection $parameters; - /** @var string[] all names of all parent elements as an array of strings, - * the last array element is the name of the element itself - */ - private array $full_path_strings = []; - - /** - * Alternative names (semicolon-separated) for this element, which can be used for searching (especially for info provider system) - */ - #[ORM\Column(type: Types::TEXT, nullable: true, options: ['default' => null])] - private ?string $alternative_names = ""; - public function __construct() { parent::__construct(); - $this->children = new ArrayCollection(); + $this->initializeStructuralElement(); $this->parameters = new ArrayCollection(); } @@ -149,307 +92,4 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement } parent::__clone(); } - - /****************************************************************************** - * StructuralDBElement constructor. - *****************************************************************************/ - - /** - * Check if this element is a child of another element (recursive). - * - * @param AbstractStructuralDBElement $another_element the object to compare - * IMPORTANT: both objects to compare must be from the same class (for example two "Device" objects)! - * - * @return bool true, if this element is child of $another_element - * - * @throws InvalidArgumentException if there was an error - */ - public function isChildOf(self $another_element): bool - { - $class_name = static::class; - - //Check if both elements compared, are from the same type - // (we have to check inheritance, or we get exceptions when using doctrine entities (they have a proxy type): - if (!$another_element instanceof $class_name && !is_a($this, $another_element::class)) { - throw new InvalidArgumentException('isChildOf() only works for objects of the same type!'); - } - - if (!$this->getParent() instanceof self) { // this is the root node - return false; - } - - //If the parent element is equal to the element we want to compare, return true - if ($this->getParent()->getID() === null) { - //If the IDs are not yet defined, we have to compare the objects itself - if ($this->getParent() === $another_element) { - return true; - } - } elseif ($this->getParent()->getID() === $another_element->getID()) { - return true; - } - - //Otherwise, check recursively - return $this->parent->isChildOf($another_element); - } - - /** - * Checks if this element is an root element (has no parent). - * - * @return bool true if this element is a root element - */ - public function isRoot(): bool - { - return $this->parent === null; - } - - /****************************************************************************** - * - * Getters - * - ******************************************************************************/ - - /** - * Get the parent of this element. - * - * @return static|null The parent element. Null if this element, does not have a parent. - */ - public function getParent(): ?self - { - return $this->parent; - } - - /** - * Get the comment of the element as markdown encoded string. - - * - * @return string the comment - */ - public function getComment(): ?string - { - return $this->comment; - } - - /** - * Get the level. - * - * The level of the root node is -1. - * - * @return int the level of this element (zero means a most top element - * [a sub element of the root node]) - */ - public function getLevel(): int - { - /* - * Only check for nodes that have a parent. In the other cases zero is correct. - */ - if (0 === $this->level && $this->parent instanceof self) { - $element = $this->parent; - while ($element instanceof self) { - /** @var AbstractStructuralDBElement $element */ - $element = $element->parent; - ++$this->level; - } - } - - return $this->level; - } - - /** - * Get the full path. - * - * @param string $delimiter the delimiter of the returned string - * - * @return string the full path (incl. the name of this element), delimited by $delimiter - */ - #[Groups(['api:basic:read'])] - #[SerializedName('full_path')] - public function getFullPath(string $delimiter = self::PATH_DELIMITER_ARROW): string - { - if ($this->full_path_strings === []) { - $this->full_path_strings = []; - $this->full_path_strings[] = $this->getName(); - $element = $this; - - $overflow = 20; //We only allow 20 levels depth - - while ($element->parent instanceof self && $overflow >= 0) { - $element = $element->parent; - $this->full_path_strings[] = $element->getName(); - //Decrement to prevent mem overflow. - --$overflow; - } - - $this->full_path_strings = array_reverse($this->full_path_strings); - } - - return implode($delimiter, $this->full_path_strings); - } - - /** - * Gets the path to this element (including the element itself). - * - * @return self[] An array with all (recursively) parent elements (including this one), - * ordered from the lowest levels (root node) first to the highest level (the element itself) - */ - public function getPathArray(): array - { - $tmp = []; - $tmp[] = $this; - - //We only allow 20 levels depth - while (!end($tmp)->isRoot() && count($tmp) < 20) { - $tmp[] = end($tmp)->parent; - } - - return array_reverse($tmp); - } - - /** - * Get all sub elements of this element. - * - * @return Collection|iterable all subelements as an array of objects (sorted by their full path) - * @psalm-return Collection - */ - public function getSubelements(): iterable - { - //If the parent is equal to this object, we would get an endless loop, so just return an empty array - //This is just a workaround, as validator should prevent this behaviour, before it gets written to the database - if ($this->parent === $this) { - return new ArrayCollection(); - } - - //@phpstan-ignore-next-line - return $this->children ?? new ArrayCollection(); - } - - /** - * @see getSubelements() - * @return Collection|iterable - * @psalm-return Collection - */ - public function getChildren(): iterable - { - return $this->getSubelements(); - } - - public function isNotSelectable(): bool - { - return $this->not_selectable; - } - - /****************************************************************************** - * - * Setters - * - ******************************************************************************/ - - /** - * Sets the new parent object. - * - * @param static|null $new_parent The new parent object - * @return $this - */ - public function setParent(?self $new_parent): self - { - /* - if ($new_parent->isChildOf($this)) { - throw new \InvalidArgumentException('You can not use one of the element childs as parent!'); - } */ - - $this->parent = $new_parent; - - //Add this element as child to the new parent - if ($new_parent instanceof self) { - $new_parent->getChildren()->add($this); - } - - return $this; - } - - /** - * Set the comment. - * - * @param string $new_comment the new comment - * - * @return $this - */ - public function setComment(string $new_comment): self - { - $this->comment = $new_comment; - - return $this; - } - - /** - * Adds the given element as child to this element. - * @param static $child - * @return $this - */ - public function addChild(self $child): self - { - $this->children->add($child); - //Children get this element as parent - $child->setParent($this); - return $this; - } - - /** - * Removes the given element as child from this element. - * @param static $child - * @return $this - */ - public function removeChild(self $child): self - { - $this->children->removeElement($child); - //Children has no parent anymore - $child->setParent(null); - return $this; - } - - /** - * @return AbstractStructuralDBElement - */ - public function setNotSelectable(bool $not_selectable): self - { - $this->not_selectable = $not_selectable; - - return $this; - } - - public function clearChildren(): self - { - $this->children = new ArrayCollection(); - - return $this; - } - - /** - * Returns a comma separated list of alternative names. - * @return string|null - */ - public function getAlternativeNames(): ?string - { - if ($this->alternative_names === null) { - return null; - } - - //Remove trailing comma - return rtrim($this->alternative_names, ','); - } - - /** - * Sets a comma separated list of alternative names. - * @return $this - */ - public function setAlternativeNames(?string $new_value): self - { - //Add a trailing comma, if not already there (makes it easier to find in the database) - if (is_string($new_value) && !str_ends_with($new_value, ',')) { - $new_value .= ','; - } - - $this->alternative_names = $new_value; - - return $this; - } } diff --git a/src/Entity/Base/AttachmentsTrait.php b/src/Entity/Base/AttachmentsTrait.php new file mode 100644 index 00000000..524e79eb --- /dev/null +++ b/src/Entity/Base/AttachmentsTrait.php @@ -0,0 +1,109 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Base; + +use App\Entity\Attachments\Attachment; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Trait providing attachments functionality. + */ +trait AttachmentsTrait +{ + /** + * @var Collection + * ORM Mapping is done in subclasses (e.g. Part) + */ + #[Groups(['full', 'import'])] + protected Collection $attachments; + + /** + * Initialize the attachments collection. + */ + protected function initializeAttachments(): void + { + $this->attachments = new ArrayCollection(); + } + + /** + * Gets all attachments associated with this element. + */ + public function getAttachments(): Collection + { + return $this->attachments; + } + + /** + * Adds an attachment to this element. + * + * @param Attachment $attachment Attachment + * + * @return $this + */ + public function addAttachment(Attachment $attachment): self + { + //Attachment must be associated with this element + $attachment->setElement($this); + $this->attachments->add($attachment); + + return $this; + } + + /** + * Removes the given attachment from this element. + * + * @return $this + */ + public function removeAttachment(Attachment $attachment): self + { + $this->attachments->removeElement($attachment); + + //Check if this is the master attachment -> remove it from master attachment too, or it can not be deleted from DB... + if (isset($this->master_picture_attachment) && $attachment === $this->master_picture_attachment) { + $this->setMasterPictureAttachment(null); + } + + return $this; + } + + /** + * Clone helper for attachments - deep clones all attachments. + */ + protected function cloneAttachments(): void + { + if (isset($this->id) && $this->id) { + $attachments = $this->attachments; + $this->attachments = new ArrayCollection(); + //Set master attachment is needed + foreach ($attachments as $attachment) { + $clone = clone $attachment; + if (isset($this->master_picture_attachment) && $attachment === $this->master_picture_attachment) { + $this->setMasterPictureAttachment($clone); + } + $this->addAttachment($clone); + } + } + } +} diff --git a/src/Entity/Base/CompanyTrait.php b/src/Entity/Base/CompanyTrait.php new file mode 100644 index 00000000..cb765def --- /dev/null +++ b/src/Entity/Base/CompanyTrait.php @@ -0,0 +1,236 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Base; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use function is_string; + +/** + * Trait for company-specific fields like address, phone, email, etc. + */ +trait CompanyTrait +{ + /** + * @var string The address of the company + */ + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] + protected string $address = ''; + + /** + * @var string The phone number of the company + */ + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] + protected string $phone_number = ''; + + /** + * @var string The fax number of the company + */ + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] + protected string $fax_number = ''; + + /** + * @var string The email address of the company + */ + #[Assert\Email] + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] + protected string $email_address = ''; + + /** + * @var string The website of the company + */ + #[Assert\Url(requireTld: false)] + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING, length: 2048)] + #[Assert\Length(max: 2048)] + protected string $website = ''; + + /** + * @var string The link to the website of an article. Use %PARTNUMBER% as placeholder for the part number. + */ + #[ORM\Column(type: Types::STRING, length: 2048)] + #[Assert\Length(max: 2048)] + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + protected string $auto_product_url = ''; + + /** + * Get the address. + * + * @return string the address of the company (with "\n" as line break) + */ + public function getAddress(): string + { + return $this->address; + } + + /** + * Set the addres. + * + * @param string $new_address the new address (with "\n" as line break) + * + * @return $this + */ + public function setAddress(string $new_address): self + { + $this->address = $new_address; + + return $this; + } + + /** + * Get the phone number. + * + * @return string the phone number of the company + */ + public function getPhoneNumber(): string + { + return $this->phone_number; + } + + /** + * Set the phone number. + * + * @param string $new_phone_number the new phone number + * + * @return $this + */ + public function setPhoneNumber(string $new_phone_number): self + { + $this->phone_number = $new_phone_number; + + return $this; + } + + /** + * Get the fax number. + * + * @return string the fax number of the company + */ + public function getFaxNumber(): string + { + return $this->fax_number; + } + + /** + * Set the fax number. + * + * @param string $new_fax_number the new fax number + * + * @return $this + */ + public function setFaxNumber(string $new_fax_number): self + { + $this->fax_number = $new_fax_number; + + return $this; + } + + /** + * Get the e-mail address. + * + * @return string the e-mail address of the company + */ + public function getEmailAddress(): string + { + return $this->email_address; + } + + /** + * Set the e-mail address. + * + * @param string $new_email_address the new e-mail address + * + * @return $this + */ + public function setEmailAddress(string $new_email_address): self + { + $this->email_address = $new_email_address; + + return $this; + } + + /** + * Get the website. + * + * @return string the website of the company + */ + public function getWebsite(): string + { + return $this->website; + } + + /** + * Set the website. + * + * @param string $new_website the new website + * + * @return $this + */ + public function setWebsite(string $new_website): self + { + $this->website = $new_website; + + return $this; + } + + /** + * Get the link to the website of an article. + * + * @param string|null $partnr * NULL for returning the URL with a placeholder for the part number + * * or the part number for returning the direct URL to the article + * + * @return string the link to the article + */ + public function getAutoProductUrl(?string $partnr = null): string + { + if (is_string($partnr)) { + return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url); + } + + return $this->auto_product_url; + } + + /** + * Set the link to the website of an article. + * + * @param string $new_url the new URL with the placeholder %PARTNUMBER% for the part number + * + * @return $this + */ + public function setAutoProductUrl(string $new_url): self + { + $this->auto_product_url = $new_url; + + return $this; + } +} diff --git a/src/Entity/Base/DBElementTrait.php b/src/Entity/Base/DBElementTrait.php new file mode 100644 index 00000000..d1f27f5b --- /dev/null +++ b/src/Entity/Base/DBElementTrait.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Base; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Trait providing basic database element functionality with an ID. + */ +trait DBElementTrait +{ + /** + * @var int|null The Identification number for this element. This value is unique for the element in this table. + * Null if the element is not saved to DB yet. + */ + #[Groups(['full', 'api:basic:read'])] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue] + protected ?int $id = null; + + /** + * Get the ID. The ID can be zero, or even negative (for virtual elements). If an element is virtual, can be + * checked with isVirtualElement(). + * + * Returns null, if the element is not saved to the DB yet. + * + * @return int|null the ID of this element + */ + public function getID(): ?int + { + return $this->id; + } + + /** + * Clone helper for DB element - resets ID on clone. + */ + protected function cloneDBElement(): void + { + if ($this->id) { + //Set ID to null, so that a new entry is created + $this->id = null; + } + } +} diff --git a/src/Entity/Base/NamedElementTrait.php b/src/Entity/Base/NamedElementTrait.php new file mode 100644 index 00000000..83416942 --- /dev/null +++ b/src/Entity/Base/NamedElementTrait.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Base; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Trait providing named element functionality. + */ +trait NamedElementTrait +{ + /** + * @var string The name of this element + */ + #[Assert\NotBlank] + #[Groups(['simple', 'extended', 'full', 'import', 'api:basic:read', 'api:basic:write'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] + protected string $name = ''; + + /** + * Get the name of this element. + * + * @return string the name of this element + */ + public function getName(): string + { + return $this->name; + } + + /** + * Change the name of this element. + * + * @param string $new_name the new name + */ + public function setName(string $new_name): self + { + $this->name = $new_name; + + return $this; + } + + /** + * String representation returns the name. + */ + public function __toString(): string + { + return $this->getName(); + } +} diff --git a/src/Entity/Base/StructuralElementTrait.php b/src/Entity/Base/StructuralElementTrait.php new file mode 100644 index 00000000..a022e49f --- /dev/null +++ b/src/Entity/Base/StructuralElementTrait.php @@ -0,0 +1,376 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Base; + +use App\Validator\Constraints\NoneOfItsChildren; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use InvalidArgumentException; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; +use function count; + +/** + * Trait for structural/hierarchical elements forming a tree structure. + */ +trait StructuralElementTrait +{ + /** + * This is a not standard character, so build a const, so a dev can easily use it. + */ + final public const PATH_DELIMITER_ARROW = ' → '; + + /** + * @var string The comment info for this element as markdown + */ + #[Groups(['full', 'import'])] + #[ORM\Column(type: Types::TEXT)] + protected string $comment = ''; + + /** + * @var bool If this property is set, this element can not be selected for part properties. + * Useful if this element should be used only for grouping, sorting. + */ + #[Groups(['full', 'import'])] + #[ORM\Column(type: Types::BOOLEAN)] + protected bool $not_selectable = false; + + /** + * @var int + */ + protected int $level = 0; + + /** + * We can not define the mapping here, or we will get an exception. Unfortunately we have to do the mapping in the + * subclasses. + * + * @var Collection + */ + #[Groups(['include_children'])] + protected Collection $children; + + /** + * @var static|null + */ + #[Groups(['include_parents', 'import'])] + #[NoneOfItsChildren] + protected ?self $parent = null; + + /** @var string[] all names of all parent elements as an array of strings, + * the last array element is the name of the element itself + */ + private array $full_path_strings = []; + + /** + * Alternative names (semicolon-separated) for this element, which can be used for searching (especially for info provider system) + */ + #[ORM\Column(type: Types::TEXT, nullable: true, options: ['default' => null])] + private ?string $alternative_names = ""; + + /** + * Initialize structural element collections. + */ + protected function initializeStructuralElement(): void + { + $this->children = new ArrayCollection(); + } + + /** + * Check if this element is a child of another element (recursive). + * + * @param self $another_element the object to compare + * IMPORTANT: both objects to compare must be from the same class (for example two "Device" objects)! + * + * @return bool true, if this element is child of $another_element + * + * @throws InvalidArgumentException if there was an error + */ + public function isChildOf(self $another_element): bool + { + $class_name = static::class; + + //Check if both elements compared, are from the same type + // (we have to check inheritance, or we get exceptions when using doctrine entities (they have a proxy type): + if (!$another_element instanceof $class_name && !is_a($this, $another_element::class)) { + throw new InvalidArgumentException('isChildOf() only works for objects of the same type!'); + } + + if (!$this->getParent() instanceof self) { // this is the root node + return false; + } + + //If the parent element is equal to the element we want to compare, return true + if ($this->getParent()->getID() === null) { + //If the IDs are not yet defined, we have to compare the objects itself + if ($this->getParent() === $another_element) { + return true; + } + } elseif ($this->getParent()->getID() === $another_element->getID()) { + return true; + } + + //Otherwise, check recursively + return $this->parent->isChildOf($another_element); + } + + /** + * Checks if this element is a root element (has no parent). + * + * @return bool true if this element is a root element + */ + public function isRoot(): bool + { + return $this->parent === null; + } + + /** + * Get the parent of this element. + * + * @return static|null The parent element. Null if this element, does not have a parent. + */ + public function getParent(): ?self + { + return $this->parent; + } + + /** + * Get the comment of the element as markdown encoded string. + * + * @return string the comment + */ + public function getComment(): ?string + { + return $this->comment; + } + + /** + * Set the comment. + * + * @param string $new_comment the new comment + * + * @return $this + */ + public function setComment(string $new_comment): self + { + $this->comment = $new_comment; + + return $this; + } + + /** + * Get the level. + * + * The level of the root node is -1. + * + * @return int the level of this element (zero means a most top element + * [a sub element of the root node]) + */ + public function getLevel(): int + { + /* + * Only check for nodes that have a parent. In the other cases zero is correct. + */ + if (0 === $this->level && $this->parent instanceof self) { + $element = $this->parent; + while ($element instanceof self) { + $element = $element->parent; + ++$this->level; + } + } + + return $this->level; + } + + /** + * Get the full path. + * + * @param string $delimiter the delimiter of the returned string + * + * @return string the full path (incl. the name of this element), delimited by $delimiter + */ + #[Groups(['api:basic:read'])] + #[SerializedName('full_path')] + public function getFullPath(string $delimiter = self::PATH_DELIMITER_ARROW): string + { + if ($this->full_path_strings === []) { + $this->full_path_strings = []; + $this->full_path_strings[] = $this->getName(); + $element = $this; + + $overflow = 20; //We only allow 20 levels depth + + while ($element->parent instanceof self && $overflow >= 0) { + $element = $element->parent; + $this->full_path_strings[] = $element->getName(); + //Decrement to prevent mem overflow. + --$overflow; + } + + $this->full_path_strings = array_reverse($this->full_path_strings); + } + + return implode($delimiter, $this->full_path_strings); + } + + /** + * Gets the path to this element (including the element itself). + * + * @return self[] An array with all (recursively) parent elements (including this one), + * ordered from the lowest levels (root node) first to the highest level (the element itself) + */ + public function getPathArray(): array + { + $tmp = []; + $tmp[] = $this; + + //We only allow 20 levels depth + while (!end($tmp)->isRoot() && count($tmp) < 20) { + $tmp[] = end($tmp)->parent; + } + + return array_reverse($tmp); + } + + /** + * Get all sub elements of this element. + * + * @return Collection|iterable all subelements as an array of objects (sorted by their full path) + */ + public function getSubelements(): iterable + { + //If the parent is equal to this object, we would get an endless loop, so just return an empty array + //This is just a workaround, as validator should prevent this behaviour, before it gets written to the database + if ($this->parent === $this) { + return new ArrayCollection(); + } + + return $this->children ?? new ArrayCollection(); + } + + /** + * @see getSubelements() + * @return Collection|iterable + */ + public function getChildren(): iterable + { + return $this->getSubelements(); + } + + /** + * Sets the new parent object. + * + * @param static|null $new_parent The new parent object + * @return $this + */ + public function setParent(?self $new_parent): self + { + $this->parent = $new_parent; + + //Add this element as child to the new parent + if ($new_parent instanceof self) { + $new_parent->getChildren()->add($this); + } + + return $this; + } + + /** + * Adds the given element as child to this element. + * @param static $child + * @return $this + */ + public function addChild(self $child): self + { + $this->children->add($child); + //Children get this element as parent + $child->setParent($this); + return $this; + } + + /** + * Removes the given element as child from this element. + * @param static $child + * @return $this + */ + public function removeChild(self $child): self + { + $this->children->removeElement($child); + //Children has no parent anymore + $child->setParent(null); + return $this; + } + + public function isNotSelectable(): bool + { + return $this->not_selectable; + } + + /** + * @return $this + */ + public function setNotSelectable(bool $not_selectable): self + { + $this->not_selectable = $not_selectable; + + return $this; + } + + public function clearChildren(): self + { + $this->children = new ArrayCollection(); + + return $this; + } + + /** + * Returns a comma separated list of alternative names. + * @return string|null + */ + public function getAlternativeNames(): ?string + { + if ($this->alternative_names === null) { + return null; + } + + //Remove trailing comma + return rtrim($this->alternative_names, ','); + } + + /** + * Sets a comma separated list of alternative names. + * @return $this + */ + public function setAlternativeNames(?string $new_value): self + { + //Add a trailing comma, if not already there (makes it easier to find in the database) + if (is_string($new_value) && !str_ends_with($new_value, ',')) { + $new_value .= ','; + } + + $this->alternative_names = $new_value; + + return $this; + } +} diff --git a/src/Entity/Contracts/CompanyInterface.php b/src/Entity/Contracts/CompanyInterface.php new file mode 100644 index 00000000..fa29487f --- /dev/null +++ b/src/Entity/Contracts/CompanyInterface.php @@ -0,0 +1,57 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Contracts; + +/** + * Interface for company entities (suppliers, manufacturers). + */ +interface CompanyInterface +{ + /** + * Get the address. + * + * @return string the address of the company (with "\n" as line break) + */ + public function getAddress(): string; + + /** + * Get the phone number. + * + * @return string the phone number of the company + */ + public function getPhoneNumber(): string; + + /** + * Get the e-mail address. + * + * @return string the e-mail address of the company + */ + public function getEmailAddress(): string; + + /** + * Get the website. + * + * @return string the website of the company + */ + public function getWebsite(): string; +} diff --git a/src/Entity/Contracts/DBElementInterface.php b/src/Entity/Contracts/DBElementInterface.php new file mode 100644 index 00000000..a36a2aff --- /dev/null +++ b/src/Entity/Contracts/DBElementInterface.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Contracts; + +/** + * Interface for entities that have a database ID. + */ +interface DBElementInterface +{ + /** + * Get the ID. The ID can be zero, or even negative (for virtual elements). + * + * Returns null, if the element is not saved to the DB yet. + * + * @return int|null the ID of this element + */ + public function getID(): ?int; +} diff --git a/src/Entity/Contracts/HasParametersInterface.php b/src/Entity/Contracts/HasParametersInterface.php new file mode 100644 index 00000000..980cfb26 --- /dev/null +++ b/src/Entity/Contracts/HasParametersInterface.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Contracts; + +use Doctrine\Common\Collections\Collection; + +/** + * Interface for entities that have parameters. + */ +interface HasParametersInterface +{ + /** + * Return all associated parameters. + * + * @return Collection + */ + public function getParameters(): Collection; +} diff --git a/src/Entity/Contracts/StructuralElementInterface.php b/src/Entity/Contracts/StructuralElementInterface.php new file mode 100644 index 00000000..d4a17181 --- /dev/null +++ b/src/Entity/Contracts/StructuralElementInterface.php @@ -0,0 +1,70 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Contracts; + +use Doctrine\Common\Collections\Collection; + +/** + * Interface for structural elements that form a tree hierarchy. + */ +interface StructuralElementInterface +{ + /** + * Get the parent of this element. + * + * @return static|null The parent element. Null if this element does not have a parent. + */ + public function getParent(): ?self; + + /** + * Get all sub elements of this element. + * + * @return Collection|iterable all subelements + */ + public function getChildren(): iterable; + + /** + * Checks if this element is a root element (has no parent). + * + * @return bool true if this element is a root element + */ + public function isRoot(): bool; + + /** + * Get the full path. + * + * @param string $delimiter the delimiter of the returned string + * + * @return string the full path (incl. the name of this element), delimited by $delimiter + */ + public function getFullPath(string $delimiter = ' → '): string; + + /** + * Get the level. + * + * The level of the root node is -1. + * + * @return int the level of this element (zero means a most top element) + */ + public function getLevel(): int; +}