Extract entity functionality into traits and interfaces

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-02 23:44:20 +00:00
parent dcafdbe7f7
commit ebbfd11545
14 changed files with 1084 additions and 740 deletions

View file

@ -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<int, Attachment>
* @phpstan-var Collection<int, AT>
* 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;
}
}

View file

@ -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<AT, PT>
*/
#[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;
}
}

View file

@ -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

View file

@ -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();
}
}

View file

@ -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<int, AbstractStructuralDBElement>
* @phpstan-var Collection<int, static>
*/
#[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<static>|iterable all subelements as an array of objects (sorted by their full path)
* @psalm-return Collection<int, static>
*/
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<static>|iterable
* @psalm-return Collection<int, static>
*/
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;
}
}

View file

@ -0,0 +1,109 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<int, Attachment>
* 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);
}
}
}
}

View file

@ -0,0 +1,236 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}
}
}

View file

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -0,0 +1,376 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<int, static>
*/
#[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<static>|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<static>|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;
}
}

View file

@ -0,0 +1,57 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}

View file

@ -0,0 +1,70 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<static>|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;
}