. */ declare(strict_types=1); namespace App\Entity\Parts; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; use ApiPlatform\Doctrine\Orm\Filter\DateFilter; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\DocumentedAPIProperty; use App\ApiPlatform\Filter\EntityFilter; use App\ApiPlatform\Filter\LikeFilter; use App\ApiPlatform\Filter\PartStoragelocationFilter; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Parts\PartTraits\AssociationTrait; use App\Repository\PartRepository; use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\PartAttachment; use App\Entity\Parts\PartTraits\ProjectTrait; use App\Entity\Parameters\ParametersTrait; use App\Entity\Parameters\PartParameter; use App\Entity\Parts\PartTraits\AdvancedPropertyTrait; use App\Entity\Parts\PartTraits\BasicPropertyTrait; use App\Entity\Parts\PartTraits\InstockTrait; use App\Entity\Parts\PartTraits\ManufacturerTrait; use App\Entity\Parts\PartTraits\OrderTrait; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Jfcherng\Diff\Utility\Arr; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * Part class. * * The class properties are split over various traits in directory PartTraits. * Otherwise, this class would be too big, to be maintained. * @see \App\Tests\Entity\Parts\PartTest * @extends AttachmentContainingDBElement * @template-use ParametersTrait */ #[UniqueEntity(fields: ['ipn'], message: 'part.ipn.must_be_unique')] #[ORM\Entity(repositoryClass: PartRepository::class)] #[ORM\Table('`parts`')] #[ORM\Index(name: 'parts_idx_datet_name_last_id_needs', columns: ['datetime_added', 'name', 'last_modified', 'id', 'needs_review'])] #[ORM\Index(name: 'parts_idx_name', columns: ['name'])] #[ORM\Index(name: 'parts_idx_ipn', columns: ['ipn'])] #[ApiResource( operations: [ new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read', 'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read'], 'openapi_definition_name' => 'Read', ], security: 'is_granted("read", object)'), new GetCollection(security: 'is_granted("@parts.read")'), new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), ], normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['part:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])] #[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "tags", "manufacturer_product_number"])] #[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])] #[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])] #[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] #[DocumentedAPIProperty(schemaName: 'Part-Read', property: 'total_instock', type: 'number', nullable: false, description: 'The total amount of this part in stock (sum of all part lots).')] class Part extends AttachmentContainingDBElement { use AdvancedPropertyTrait; //use MasterAttachmentTrait; use BasicPropertyTrait; use InstockTrait; use ManufacturerTrait; use OrderTrait; use ParametersTrait; use ProjectTrait; use AssociationTrait; /** @var Collection */ #[Assert\Valid] #[Groups(['full', 'part:read', 'part:write'])] #[ORM\OneToMany(targetEntity: PartParameter::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)] #[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])] protected Collection $parameters; /** ************************************************************* * Overridden properties * (They are defined here and not in a trait, to avoid conflicts). ****************************************************************/ /** * @var string The name of this part */ protected string $name = ''; /** * @var Collection */ #[Assert\Valid] #[Groups(['full', 'part:read', 'part:write'])] #[ORM\OneToMany(targetEntity: PartAttachment::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)] #[ORM\OrderBy(['name' => 'ASC'])] protected Collection $attachments; /** * @var Attachment|null */ #[Assert\Expression('value == null or value.isPicture()', message: 'part.master_attachment.must_be_picture')] #[ORM\ManyToOne(targetEntity: PartAttachment::class)] #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] #[Groups(['part:read', 'part:write'])] protected ?Attachment $master_picture_attachment = null; #[Groups(['part:read'])] protected ?\DateTimeInterface $addedDate = null; #[Groups(['part:read'])] protected ?\DateTimeInterface $lastModified = null; public function __construct() { $this->attachments = new ArrayCollection(); parent::__construct(); $this->partLots = new ArrayCollection(); $this->orderdetails = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->project_bom_entries = new ArrayCollection(); $this->associated_parts_as_owner = new ArrayCollection(); $this->associated_parts_as_other = new ArrayCollection(); //By default, the part has no provider $this->providerReference = InfoProviderReference::noProvider(); } public function __clone() { if ($this->id) { //Deep clone part lots $lots = $this->partLots; $this->partLots = new ArrayCollection(); foreach ($lots as $lot) { $this->addPartLot(clone $lot); } //Deep clone order details $orderdetails = $this->orderdetails; $this->orderdetails = new ArrayCollection(); foreach ($orderdetails as $orderdetail) { $this->addOrderdetail(clone $orderdetail); } //Deep clone parameters $parameters = $this->parameters; $this->parameters = new ArrayCollection(); foreach ($parameters as $parameter) { $this->addParameter(clone $parameter); } //Deep clone the owned part associations (the owned ones make not much sense without the owner) $ownedAssociations = $this->associated_parts_as_owner; $this->associated_parts_as_owner = new ArrayCollection(); foreach ($ownedAssociations as $association) { $this->addAssociatedPartsAsOwner(clone $association); } //Deep clone info provider $this->providerReference = clone $this->providerReference; } parent::__clone(); } #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { //Ensure that the part name fullfills the regex of the category if ($this->category instanceof Category) { $regex = $this->category->getPartnameRegex(); if ($regex !== '' && !preg_match($regex, $this->name)) { $context->buildViolation('part.name.must_match_category_regex') ->atPath('name') ->setParameter('%regex%', $regex) ->addViolation(); } } } }