'Switch the status of the product'], normalizationContext: ['groups' => [ProductSwitchProcessor::class]], security: "is_granted('".ProductVoter::EDIT."', object)", input: false, processor: ProductSwitchProcessor::class, ), ] )] class Product implements \Stringable, ImagesInterface { use TimestampableEntity; use ProductObjectTrait; use ProductServiceTrait; use i18nTrait; final public const DEFAULT_CURRENCY = 'EUR'; /** * Generates a V6 uuid. */ #[ORM\Id] #[ORM\Column(type: 'uuid', unique: true)] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: UuidGenerator::class)] #[ApiProperty(identifier: true)] #[Groups([ProductSwitchProcessor::class])] private Uuid $id; /** * Type of the product. It can be an object to lend or a service. */ #[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: ProductType::class)] #[Assert\NotBlank] protected ProductType $type; /** * Main category of the product (1st or second level). */ #[ORM\ManyToOne(targetEntity: Category::class)] #[ORM\JoinColumn(referencedColumnName: 'id', nullable: false)] #[Assert\NotBlank(groups: [AbstractProductFormType::class])] private Category $category; /** * Status of the product. */ #[ORM\Column(name: 'status', type: 'string', nullable: false, enumType: ProductStatus::class)] #[Assert\NotBlank] #[Groups([ProductSwitchProcessor::class])] protected ProductStatus $status; /** * Visibility of the product. */ #[ORM\Column(name: 'visibility', type: 'string', nullable: false, enumType: ProductVisibility::class)] #[Assert\NotBlank] protected ProductVisibility $visibility = ProductVisibility::PUBLIC; /** * User that owns the product or propose the service. */ #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] // if the owner is deleted then the product will also be deleted without constraint error #[Assert\NotBlank] protected User $owner; /** * Short and main name of the product. */ #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] #[Assert\NotBlank(groups: [AbstractProductFormType::class])] #[Assert\Length(max: 255, groups: [AbstractProductFormType::class, 'default'])] private string $name; /** * SEO friendly name for URLs. */ #[ORM\Column(length: 255, unique: false)] #[Gedmo\Slug(fields: ['name'])] private string $slug; /** * Longer description of the product. */ #[ORM\Column(type: Types::TEXT, nullable: true)] #[Assert\NotBlank(groups: [AbstractProductFormType::class])] #[Assert\Length(max: 2000, groups: [AbstractProductFormType::class])] private ?string $description = null; /** * User images for the product. * * @var array */ #[ORM\Column(type: 'json', nullable: true)] private ?array $images = null; /** * @var Collection $availabilities */ #[ORM\OneToMany(mappedBy: 'product', targetEntity: ProductAvailability::class, cascade: ['persist', 'remove', 'detach'])] #[ORM\OrderBy(['startAt' => 'ASC'])] private Collection $availabilities; /** * @var Collection $serviceRequests */ #[ORM\OneToMany(mappedBy: 'product', targetEntity: ServiceRequest::class)] private Collection $serviceRequests; /** * If the product in not public then the list of group the product is visible. * * @var Collection $groups */ #[ORM\ManyToMany(targetEntity: Group::class, inversedBy: 'products')] #[Assert\When( expression: '!this.getVisibility().isPublic() && !this.getOwner().getUserGroupsConfirmedWithServices().isEmpty()', constraints: [ new Assert\Count(min: 1, minMessage: 'app.entity.product.groups.constraints.count.min_message'), ], groups: [AbstractProductFormType::class], )] private Collection $groups; /** * This is a virtual field to store the distance with a given location when * using a proximity filter. */ private ?int $geoDistance = null; /** * Get distance in meters. */ public function setGeoDistance(?int $geoDistance): self { $this->geoDistance = $geoDistance; return $this; } /** * Return kilometers. */ public function getGeoDistance(): ?float { return $this->geoDistance !== null ? $this->geoDistance / 1000 : null; } public function __construct() { $this->availabilities = new ArrayCollection(); $this->serviceRequests = new ArrayCollection(); $this->groups = new ArrayCollection(); } public function __toString(): string { return $this->name; } public function getId(): Uuid { return $this->id; } public function setId(Uuid $uuid): self { $this->id = $uuid; return $this; } public function getType(): ProductType { return $this->type; } public function setType(ProductType $type): self { $this->type = $type; return $this; } public function getCategory(): Category { return $this->category; } public function setCategory(Category $category): self { $this->category = $category; return $this; } public function getStatus(): ProductStatus { return $this->status; } public function setStatus(ProductStatus $status): Product { $this->status = $status; return $this; } public function getVisibility(): ProductVisibility { return $this->visibility; } public function setVisibility(ProductVisibility $visibility): self { $this->visibility = $visibility; return $this; } public function getOwner(): User { return $this->owner; } public function isOwner(User $user): bool { return $this->owner === $user; } public function setOwner(User $owner): self { $this->owner = $owner; return $this; } public function getName(): string { return $this->name; } public function setName(string $name): self { $this->name = $name; return $this; } public function setSlug(string $slug): self { $this->slug = $slug; return $this; } public function getSlug(): string { return $this->slug; } public function getDescription(): ?string { return $this->description; } public function setDescription(?string $description): self { $this->description = $description; return $this; } public function getImages(): ?array { return $this->images; } public function getFirstImage(): ?string { return $this->images[0] ?? null; } /** * @param array|null $images */ public function setImages(?array $images): self { $this->images = array_values(array_filter($images ?? [])); // make sure we don't save null or empty values return $this; } /** * @param array $images */ public function addImages(array $images): self { $this->images = array_merge($this->images ?? [], $images); return $this; } /** * @return Collection */ public function getAvailabilities(): Collection { return $this->availabilities; } /** * @param Collection $availabilities */ public function setAvailabilities(Collection $availabilities): self { $this->availabilities = $availabilities; return $this; } public function addAvailability(ProductAvailability $productAvailability): self { if (!$this->availabilities->contains($productAvailability)) { $this->availabilities->add($productAvailability); $productAvailability->setProduct($this); } return $this; } public function removeAvailability(ProductAvailability $productAvailability): self { $this->availabilities->removeElement($productAvailability); return $this; } /** * @return Collection */ public function getServiceRequests(): Collection { return $this->serviceRequests; } /** * @param Collection $serviceRequests */ public function setServiceRequests(Collection $serviceRequests): void { $this->serviceRequests = $serviceRequests; } /** * @return Collection */ public function getGroups(): Collection { return $this->groups; } /** * @return array */ public function getGroupsIds(): array { return $this->getGroups()->map(fn (Group $group) => (string) $group->getId())->toArray(); } public function addGroup(Group $group): self { if (!$this->groups->contains($group)) { $this->groups->add($group); } return $this; } public function removeGroup(Group $group): self { $this->groups->removeElement($group); return $this; } /** * Remove all associated groups. */ public function removeGroups(): self { $this->groups->clear(); return $this; } public function setPublic(): self { $this->visibility = ProductVisibility::PUBLIC; return $this; } public function setPaused(): self { $this->status = ProductStatus::PAUSED; return $this; } public function setActive(): self { $this->status = ProductStatus::ACTIVE; return $this; } /* End of basic getters/setters ========================================= */ public function isActive(): bool { return $this->getStatus()->isActive(); } public function isPaused(): bool { return $this->getStatus()->isPaused(); } public function switchStatus(): self { $this->setStatus($this->isActive() ? ProductStatus::PAUSED : ProductStatus::ACTIVE); return $this; } /** * Return the unavailabilities of the product as a simple array of dates "2023-02-09". * * @return array */ public function getUnavailabilities(?ServiceRequest $serviceRequest = null): array { /** @var array $resultArray */ $resultArray = []; $today = CarbonImmutable::today(); // start of day 00:00:00 $unavailabilities = $this->getAvailabilities()->filter( fn (ProductAvailability $pa) => $pa->getMode()->isUnavailable() // of the good type && ($serviceRequest === null || $pa->getServiceRequest() !== $serviceRequest) // exclude the dates of the current service request (modify dates) && $pa->getEndAt() >= $today // passed dates are useless but the start date can be in the past ); foreach ($unavailabilities as $unavailability) { /** @var ProductAvailability $unavailability */ $period = CarbonInterval::days()->toPeriod($unavailability->getStartAt(), $unavailability->getEndAt()); $resultArray = array_merge($resultArray, $period->toArray()); } $resultArray = array_map(static fn (CarbonInterface $date) => $date->format('Y-m-d'), $resultArray); sort($resultArray); return array_unique($resultArray); } /** * Get 1st level category. */ public function getMainCategory(): Category { return $this->category->getParent() ?? $this->category; } /** * Get subcategory, it is the current category if it is a child. */ public function getSubCategory(): ?Category { return $this->category->hasParent() ? $this->category : null; } public function createServiceRequest(User $recipient, \DateTimeImmutable $startAt, \DateTimeImmutable $endAt): ServiceRequest { return (new ServiceRequest()) ->setOwner($this->getOwner()) ->setProduct($this) ->setRecipient($recipient) ->setStartAt($startAt) ->setEndAt($endAt); } public function duplicate(): self { return (new Product()) ->setType($this->getType()) ->setCategory($this->getCategory()) ->setOwner($this->getOwner()) ->setType($this->getType()) ->setStatus($this->getStatus()) ->setVisibility($this->getVisibility()) ->setDescription($this->getDescription()) ->setAge($this->getAge()) ->setDeposit($this->getDeposit()) ->setCurrency($this->getCurrency()) ->setPreferredLoanDuration($this->getPreferredLoanDuration()) ->setDuration($this->getDuration()); } public function deleteImage(string $image): self { $images = array_flip($this->images ?? []); unset($images[$image]); $this->images = array_values(array_flip($images)); return $this; } public function delete(): self { $this->status = ProductStatus::DELETED; return $this; } public function hasServiceRequests(): bool { return !$this->serviceRequests->isEmpty(); } public function hasOngoingServiceRequests(): bool { $ongoing = $this->serviceRequests->filter( fn (ServiceRequest $serviceRequest) => $serviceRequest->getStatus()->isOngoing() ); return !$ongoing->isEmpty(); } /** * A product is indexable if it is active and the owner has not activated the * vacation mode. */ public function isIndexable(): bool { return $this->status->isIndexable() && $this->owner->isIndexable() ; } /** * @return array */ public function getRoutingParameters(): array { return [ 'id' => (string) $this->getId(), 'slug' => $this->getSlug(), ]; } }