src/Entity/Profile/Profile.php line 74

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by simpson <simpsonwork@gmail.com>
  4.  * Date: 2019-03-19
  5.  * Time: 15:36
  6.  */
  7. namespace App\Entity\Profile;
  8. use AngelGamez\TranslatableBundle\Entity\TranslatableValue;
  9. //use ApiPlatform\Core\Annotation\ApiProperty;
  10. use App\Entity\Account\Advertiser;
  11. use App\Entity\ApartmentsPricing;
  12. use App\Entity\ContainsDomainEvents;
  13. use App\Entity\Account\Customer;
  14. use App\Entity\DomainEventsRecorderTrait;
  15. use App\Entity\ExpressPricing;
  16. use App\Entity\IProvidesServices;
  17. use App\Entity\Location\City;
  18. use App\Entity\Location\MapCoordinate;
  19. use App\Entity\Location\Station;
  20. use App\Entity\Messengers;
  21. use App\Entity\PhoneCallRestrictions;
  22. use App\Entity\Profile\Comment\CommentByCustomer;
  23. use App\Entity\Profile\Confirmation\ModerationRequest;
  24. use App\Entity\Sales\Profile\AdBoardPlacement;
  25. use App\Entity\Sales\Profile\PlacementHiding;
  26. use App\Entity\Sales\Profile\TopPlacement;
  27. use App\Entity\ProvidedServiceTrait;
  28. use App\Entity\TakeOutPricing;
  29. use App\Repository\ProfileRepository;
  30. use Carbon\Carbon;
  31. use Carbon\CarbonImmutable;
  32. use Doctrine\Common\Collections\ArrayCollection;
  33. use Doctrine\Common\Collections\Collection;
  34. use Doctrine\ORM\Mapping as ORM;
  35. use Doctrine\ORM\Mapping\Index;
  36. //use ApiPlatform\Core\Annotation\ApiResource;
  37. //use ApiPlatform\Core\Annotation\ApiFilter;
  38. //use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
  39. //use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter;
  40. use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
  41. use Symfony\Component\Serializer\Annotation\Groups;
  42. use Gedmo\Mapping\Annotation as Gedmo;
  43. use App\Validator\Constraints\ValidPhoneForCountry as ValidPhoneForCountryAssert;
  44. use App\Validator\Constraints\PhoneNotBlack as PhoneNotBlackAssert;
  45. /**
  46.  * ApiResource(collectionOperations={"get"}, itemOperations={"get"}, normalizationContext={"groups"={"profile"}}, attributes={"pagination_client_enabled"=true, "pagination_client_items_per_page"=true})
  47.  * ApiFilter(SearchFilter::class, properties={"city": "exact", "providedServices": "exact"})
  48.  * ApiFilter(RangeFilter::class, properties={"personParameters.age", "personParameters.height", "personParameters.weight", "personParameters.breastSize", "apartmentsPricing.oneHourPrice"})
  49.  * @ValidPhoneForCountryAssert/ProtocolClass
  50.  * @PhoneNotBlackAssert/ProtocolClass
  51.  */
  52. #[Gedmo\SoftDeleteable(fieldName"deletedAt"timeAwaretrue)]
  53. #[ORM\Table(name'profiles')]
  54. #[Index(name'idx_deleted_at'columns: ['deleted_at'])]
  55. #[Index(name'idx_created_at'columns: ['created_at'])]
  56. #[Index(name'idx_gender'columns: ['person_gender'])]
  57. #[Index(name'idx_apartments_one_hour_price'columns: ['apartments_one_hour_price'])]
  58. #[Index(name'idx_is_dummy'columns: ['is_dummy'])]
  59. #[Index(name'idx_city_deleted'columns: ['city_id''deleted_at'])]
  60. #[Index(name'idx_city_deleted_moderation'columns: ['city_id''deleted_at''moderation_status'])]
  61. #[Index(name'idx_city_masseur_deleted'columns: ['city_id''is_masseur''deleted_at'])]
  62. #[Index(name'idx_city_masseur_deleted_moderation'columns: ['city_id''is_masseur''deleted_at''moderation_status'])]
  63. #[Index(name'idx_city_deleted_gender'columns: ['city_id''deleted_at''person_gender'])]
  64. #[Index(name'idx_city_deleted_moderation_gender'columns: ['city_id''deleted_at''moderation_status''person_gender'])]
  65. #[Index(name'idx_city_masseur_deleted_gender'columns: ['city_id''is_masseur''deleted_at''person_gender'])]
  66. #[Index(name'idx_city_masseur_deleted_moderation_gender'columns: ['city_id''is_masseur''deleted_at''moderation_status''person_gender'])]
  67. #[ORM\Entity(repositoryClassProfileRepository::class)]
  68. #[ORM\HasLifecycleCallbacks]
  69. class Profile implements ContainsDomainEventsIProvidesServices
  70. {
  71.     use SoftDeleteableEntity;
  72.     use DomainEventsRecorderTrait;
  73.     use ProvidedServiceTrait;
  74.     const MODERATION_STATUS_NOT_PASSED 0;
  75.     const MODERATION_STATUS_APPROVED 1;
  76.     const MODERATION_STATUS_WAITING 2;
  77.     const MODERATION_STATUS_REJECTED 3;
  78.     #[ORM\Id]
  79.     #[ORM\Column(name'id'type'integer')]
  80.     #[ORM\GeneratedValue(strategy'AUTO')]
  81.     #[Groups('profile')]
  82.     protected int $id;
  83.     #[ORM\JoinColumn(name'user_id'referencedColumnName'id'nullabletrue)]
  84.     #[ORM\ManyToOne(targetEntityAdvertiser::class, inversedBy'profiles')]
  85.     protected ?Advertiser $owner;
  86.     #[ORM\Column(name'is_dummy'type'boolean'options: ['default' => 0])]
  87.     protected bool $dummy false;
  88.     /** @var TopPlacement[] */
  89.     #[ORM\OneToMany(targetEntityTopPlacement::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  90.     protected Collection $topPlacements;
  91.     #[ORM\OneToOne(targetEntityAdBoardPlacement::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  92.     protected ?AdBoardPlacement $adBoardPlacement null;
  93.     #[ORM\OneToOne(targetEntityPlacementHiding::class, mappedBy'profile'cascade: ['all'])]
  94.     protected ?PlacementHiding $placementHiding null;
  95.     #[ORM\Column(name'uri_identity'type'string'length64)]
  96.     #[Groups('profile')]
  97.     protected string $uriIdentity;
  98.     #[ORM\Column(name'name'type'translatable')]
  99.     #[Groups('profile')]
  100.     protected TranslatableValue $name;
  101.     #[ORM\Column(name'description'type'translatable')]
  102.     #[Groups('profile')]
  103.     protected ?TranslatableValue $description null;
  104.     #[ORM\Embedded(class: PersonParameters::class, columnPrefix'person_')]
  105.     #[Groups('profile')]
  106.     protected PersonParameters $personParameters;
  107.     /** var ProfileService[] */
  108.     #[ORM\OneToMany(targetEntityProfileService::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  109.     #[ORM\Cache(usage'NONSTRICT_READ_WRITE'region'profiles')]
  110.     protected Collection $providedServices;
  111.     /** @var int[] */
  112.     #[ORM\Column(name'client_types'type'simple_array'nullabletrue)]
  113.     protected ?array $clientTypes;
  114.     #[ORM\Column(name'phone_number'type'string'length24)]
  115.     #[Groups('profile')]
  116.     protected string $phoneNumber;
  117.     #[ORM\Embedded(class: Messengers::class, columnPrefixfalse)]
  118.     #[Groups('profile')]
  119.     protected ?Messengers $messengers null;
  120.     #[ORM\Embedded(class: PhoneCallRestrictions::class, columnPrefixfalse)]
  121.     protected ?PhoneCallRestrictions $phoneCallRestrictions null;
  122.     #[ORM\Column(name'is_masseur'type'boolean')]
  123.     protected bool $masseur false;
  124.     #[ORM\Embedded(class: ClientRestrictions::class, columnPrefixfalse)]
  125.     protected ?ClientRestrictions $clientRestrictions null;
  126.     #[ORM\Embedded(class: ApartmentsPricing::class, columnPrefixfalse)]
  127.     #[Groups('profile')]
  128.     protected ?ApartmentsPricing $apartmentsPricing null;
  129.     #[ORM\Embedded(class: TakeOutPricing::class, columnPrefixfalse)]
  130.     #[Groups('profile')]
  131.     protected ?TakeOutPricing $takeOutPricing null;
  132.     #[ORM\Embedded(class: ExpressPricing::class, columnPrefixfalse)]
  133.     #[Groups('profile')]
  134.     protected ?ExpressPricing $expressPricing null;
  135.     #[ORM\Embedded(class: CarPricing::class, columnPrefixfalse)]
  136.     #[Groups('profile')]
  137.     protected ?CarPricing $carPricing null;
  138.     #[ORM\Column(name'extra_charge'type'integer'nullabletrue)]
  139.     #[Groups('profile')]
  140.     protected ?int $extraCharge;
  141.     /** @var Photo[] */
  142.     #[ORM\OneToMany(targetEntityPhoto::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  143.     #[Groups('profile')]
  144.     protected Collection $photos;
  145.     /** @var Selfie[] */
  146.     #[ORM\OneToMany(targetEntitySelfie::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  147.     #[Groups('profile')]
  148.     protected Collection $selfies;
  149.     /** @var Video[] */
  150.     #[ORM\OneToMany(targetEntityVideo::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  151.     protected Collection $videos;
  152.     #[ORM\OneToMany(targetEntityFileProcessingTask::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  153.     protected Collection $processingFiles;
  154.     #[ORM\OneToOne(targetEntityAdminApprovalPhoto::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  155.     protected ?AdminApprovalPhoto $adminApprovalPhoto null;
  156.     #[ORM\OneToOne(targetEntityAvatar::class, mappedBy'profile'cascade: ['all'], orphanRemovaltrue)]
  157.     protected ?Avatar $avatar null;
  158.     /** @var CommentByCustomer[] */
  159.     #[ORM\OneToMany(targetEntityCommentByCustomer::class, mappedBy'profile')]
  160.     protected Collection $comments;
  161.     #[ORM\Column(name'is_approved'type'boolean')]
  162.     #[Groups('profile')]
  163.     protected bool $approved false;
  164.     #[ORM\Column(name'moderation_status'type'integer')]
  165.     #[Groups('profile')]
  166.     protected int $moderationStatus 0;
  167.     #[ORM\JoinColumn(name'city_id'referencedColumnName'id')]
  168.     #[ORM\ManyToOne(targetEntityCity::class)]
  169.     #[ORM\Cache(usage'NONSTRICT_READ_WRITE'region'profiles')]
  170.     protected City $city;
  171.     /** @var Station[] */
  172.     //, indexBy="id"
  173.     #[ORM\JoinTable(name'profile_stations')]
  174.     #[ORM\JoinColumn(name'profile_id'referencedColumnName'id')]
  175.     #[ORM\InverseJoinColumn(name'station_id'referencedColumnName'id')]
  176.     #[ORM\ManyToMany(targetEntityStation::class)]
  177.     #[Groups('profile')]
  178.     #[ORM\Cache(usage'NONSTRICT_READ_WRITE'region'profiles')]
  179.     protected Collection $stations;
  180.     #[ORM\Embedded(class: MapCoordinate::class, columnPrefixfalse)] // ApiProperty()
  181.     #[Groups('profile')]
  182.     protected ?MapCoordinate $mapCoordinate;
  183.     #[ORM\Column(name'created_at'type'datetimetz_immutable'nullabletrue)]
  184.     protected ?\DateTimeImmutable $createdAt;
  185.     #[Gedmo\Timestampable(on"change"field: ["name""description""personParameters""providedServices""clientTypes""phoneNumber""messengers""phoneCallrestrictions""masseur""clientRestrictions""apartmentsPricing""takeOutPricing""expressPricing""carPricing""extraCharge""photos""selfies""videos""avatar""stations""mapCoordinate"])]
  186.     #[ORM\Column(name'updated_at'type'datetimetz_immutable'nullabletrue)]
  187.     #[Groups('profile')]
  188.     protected ?\DateTimeImmutable $updatedAt;
  189.     #[ORM\Column(name'inactivated_at'type'datetimetz_immutable'nullabletrue)]
  190.     protected ?\DateTimeImmutable $inactivatedAt;
  191.     private bool $draft false;
  192.     #[ORM\Column(name'seo'type'json'nullabletrue)]
  193.     #[Groups('profile')]
  194.     private ?array $seo null;
  195.     #[ORM\ManyToOne(targetEntityStation::class)]
  196.     #[ORM\JoinColumn(name'primary_station_id'referencedColumnName'id'nullabletrueonDelete'SET NULL')]
  197.     #[Groups('profile')]
  198.     #[ORM\Cache(usage'READ_ONLY')]
  199.     private ?Station $primaryStation null;
  200.     protected function __construct(?\DateTimeImmutable $createdAt)
  201.     {
  202.         $this->draft true;
  203.         $this->createdAt $createdAt;
  204.         $this->photos = new ArrayCollection();
  205.         $this->selfies = new ArrayCollection();
  206.         $this->videos = new ArrayCollection();
  207.         $this->processingFiles = new ArrayCollection();
  208.         $this->comments = new ArrayCollection();
  209.         $this->topPlacements = new ArrayCollection();
  210.         $this->providedServices = new ArrayCollection();
  211.         $this->stations = new ArrayCollection();
  212.         $this->inactivatedAt CarbonImmutable::now();
  213.     }
  214.     public static function draft(?\DateTimeImmutable $createdAt null, ?bool $dummy null): self
  215.     {
  216.         $profile = new static($createdAt);
  217.         if (null !== $dummy)
  218.             $profile->dummy $dummy;
  219.         return $profile;
  220.     }
  221.     public static function create(string $uriIdentity, ?\DateTimeImmutable $createdAt null): self
  222.     {
  223.         $profile = new static($createdAt);
  224.         $profile->defineUriIdentity($uriIdentity);
  225.         $profile->toggleMasseur(false);
  226.         return $profile;
  227.     }
  228.     public function defineUriIdentity(string $uriIdentity): void
  229.     {
  230.         if (!$this->isDraft()) {
  231.             throw new \DomainException('Profile is already created and can\'t change its URI.');
  232.         }
  233.         $this->uriIdentity $uriIdentity;
  234.         $this->draft false;
  235.     }
  236.     public function isDraft(): bool
  237.     {
  238.         return $this->draft;
  239.     }
  240.     public function toggleMasseur(bool $isMasseur): void
  241.     {
  242.         if ($this->masseur !== $isMasseur && (null !== $this->adBoardPlacement && false == $this->adBoardPlacement->getType()->isFree())) {
  243.             throw new \DomainException('Impossible to toggle profile type while it is displaying on adboard.');
  244.         }
  245.         $this->masseur $isMasseur;
  246.     }
  247.     public static function createMasseur(string $uriIdentity, ?\DateTimeImmutable $createdAt null): self
  248.     {
  249.         $profile = new static($createdAt);
  250.         $profile->defineUriIdentity($uriIdentity);
  251.         $profile->toggleMasseur(true);
  252.         return $profile;
  253.     }
  254.     public function isOwnedBy(Advertiser $account): bool
  255.     {
  256.         return $account->getId() === $this->owner->getId();
  257.     }
  258.     public function getId(): int
  259.     {
  260.         return $this->id;
  261.     }
  262.     public function setBio(TranslatableValue $nameTranslatableValue $description): void
  263.     {
  264.         $this->name $name;
  265.         $this->description $description;
  266.     }
  267.     public function setLocation(City $city$stations, ?MapCoordinate $mapCoordinate): void
  268.     {
  269.         if (!$this->isDraft() && !$this->city->equals($city)) {
  270.             throw new \DomainException('City change for a saved profile is forbidden.');
  271.         }
  272.         $this->city $city;
  273.         $this->changeStations($stations);
  274.         $this->normalizePrimaryStation();
  275.         $this->mapCoordinate $mapCoordinate;
  276.     }
  277.     protected function changeStations($stations)
  278.     {
  279.         if (null === $stations)
  280.             return;
  281.         if (false === is_array($stations) && false === is_iterable($stations))
  282.             throw new \InvalidArgumentException('Stations list should be either an array or an ArrayCollection');
  283.         $stationsArray is_iterable($stations) && !is_array($stations) ? iterator_to_array($stations) : $stations;
  284.         $stations = [];
  285.         foreach ($stationsArray as $station) {
  286.             $stations[$station->getId()] = $station;
  287.         }
  288.         $stationIds array_map(function (Station $station): int {
  289.             return $station->getId();
  290.         }, $stations);
  291.         $existingStationIds $this->stations->map(function (Station $station): int {
  292.             return $station->getId();
  293.         })->getValues();
  294.         $stationIdsToAdd array_diff($stationIds$existingStationIds);
  295.         $stationIdsToRemove array_diff($existingStationIds$stationIds);
  296.         foreach ($stationIdsToAdd as $stationId) {
  297.             $this->stations->add($stations[$stationId]);
  298.         }
  299.         foreach ($stationIdsToRemove as $stationId) {
  300.             $this->stations->remove($stationId);
  301.         }
  302.     }
  303.     public function normalizePrimaryStation(): void
  304.     {
  305.         if ($this->stations->isEmpty()) {
  306.             $this->primaryStation null;
  307.             return;
  308.         }
  309.         if ($this->primaryStation === null || !$this->stations->contains($this->primaryStation)) {
  310.             $this->primaryStation $this->stations->first();
  311.         }
  312.     }
  313.     public function setEnabledProvidedServices($services): void
  314.     {
  315.         if (null !== $services) {
  316.             if (is_array($services)) {
  317.                 $services = new ArrayCollection($services);
  318.             } elseif (!$services instanceof ArrayCollection) {
  319.                 if (is_iterable($services)) {
  320.                     $services = new ArrayCollection(iterator_to_array($services));
  321.                 } else {
  322.                     throw new \InvalidArgumentException('Services list should be either an array or an ArrayCollection');
  323.                 }
  324.             }
  325.             $this->providedServices $services;
  326.         }
  327.     }
  328.     public function setPhoneCallOptions(string $phoneNumber, ?PhoneCallRestrictions $restrictions, ?Messengers $messengers): void
  329.     {
  330.         $this->phoneNumber $phoneNumber;
  331.         $this->phoneCallRestrictions $restrictions;
  332.         $this->messengers $messengers;
  333.     }
  334.     public function setPricing(?ApartmentsPricing $apartmentsPricing, ?TakeOutPricing $takeOutPricing, ?int $extraCharge, ?ExpressPricing $expressPricing null, ?CarPricing $carPricing null): void
  335.     {
  336.         $this->apartmentsPricing $apartmentsPricing;
  337.         $this->takeOutPricing $takeOutPricing;
  338.         $this->extraCharge $extraCharge;
  339.         $this->expressPricing $expressPricing;
  340.         $this->carPricing $carPricing;
  341.     }
  342.     public function isApproved(): bool
  343.     {
  344.         return $this->approved;
  345.     }
  346.     public function approve(): void
  347.     {
  348.         $this->approved true;
  349.     }
  350.     public function unApprove(): void
  351.     {
  352.         $this->approved false;
  353.     }
  354.     public function getOwner(): ?Advertiser
  355.     {
  356.         return $this->owner;
  357.     }
  358.     public function setOwner(Advertiser $owner): void
  359.     {
  360.         $this->owner $owner;
  361.     }
  362.     public function hasOwner(): bool
  363.     {
  364.         return null !== $this->owner;
  365.     }
  366.     public function getTopPlacements(): Collection
  367.     {
  368.         return $this->topPlacements;
  369.     }
  370.     public function addTopPlacement(TopPlacement $topPlacement): void
  371.     {
  372.         $this->topPlacements->add($topPlacement);
  373.     }
  374.     public function getAdBoardPlacement(): ?AdBoardPlacement
  375.     {
  376.         return $this->adBoardPlacement;
  377.     }
  378.     public function setAdBoardPlacement(AdBoardPlacement $adBoardPlacement): void
  379.     {
  380.         $this->adBoardPlacement $adBoardPlacement;
  381.     }
  382.     /**
  383.      * Анкета оплачена и выводится в общих списках на сайте
  384.      * или в ТОПе, то есть "АКТИВНА"
  385.      */
  386.     public function isActive(): bool
  387.     {
  388.         return null !== $this->adBoardPlacement || $this->hasRunningTopPlacement();
  389.     }
  390.     public function hasRunningTopPlacement(): bool
  391.     {
  392.         $now = new \DateTimeImmutable('now');
  393.         foreach ($this->topPlacements as /** @var TopPlacement $topPlacement */ $topPlacement) {
  394.             if ($topPlacement->getPlacedAt() <= $now && $now <= $topPlacement->getExpiresAt())
  395.                 return true;
  396.         }
  397.         return false;
  398.     }
  399.     public function getUriIdentity(): string
  400.     {
  401.         return $this->uriIdentity;
  402.     }
  403.     public function getName(): TranslatableValue
  404.     {
  405.         return $this->name;
  406.     }
  407.     public function getDescription(): ?TranslatableValue
  408.     {
  409.         return $this->description;
  410.     }
  411.     public function getPersonParameters(): PersonParameters
  412.     {
  413.         return $this->personParameters;
  414.     }
  415.     public function setPersonParameters(PersonParameters $personParameters): void
  416.     {
  417.         $this->personParameters $personParameters;
  418.     }
  419.     public function getPhoneNumber(): string
  420.     {
  421.         return $this->phoneNumber;
  422.     }
  423.     //TODO return type
  424.     public function getPhoneCallRestrictions(): ?PhoneCallRestrictions
  425.     {
  426.         return $this->phoneCallRestrictions;
  427.     }
  428.     public function isMasseur(): bool
  429.     {
  430.         return $this->masseur;
  431.     }
  432.     //TODO return type
  433.     public function getClientRestrictions(): ?ClientRestrictions
  434.     {
  435.         return $this->clientRestrictions;
  436.     }
  437.     //TODO return type
  438.     public function setClientRestrictions(?ClientRestrictions $restrictions): void
  439.     {
  440.         $this->clientRestrictions $restrictions;
  441.     }
  442.     //TODO return type
  443.     public function getApartmentsPricing(): ?ApartmentsPricing
  444.     {
  445.         return $this->apartmentsPricing;
  446.     }
  447.     public function getTakeOutPricing(): ?TakeOutPricing
  448.     {
  449.         return $this->takeOutPricing;
  450.     }
  451.     public function getExtraCharge(): ?int
  452.     {
  453.         return $this->extraCharge;
  454.     }
  455.     public function addPhoto(string $pathbool $isMain): Photo
  456.     {
  457.         $photos $this->getPhotos();
  458.         $found $photos->filter(function (Photo $photo) use ($path): bool {
  459.             return $path === $photo->getPath();
  460.         });
  461.         if (!$found->isEmpty())
  462.             return $found->first();
  463.         if (true === $isMain) {
  464.             $photos->forAll(function ($indexPhoto $photo): true {
  465.                 $photo->unsetMain();
  466.                 return true;
  467.             });
  468.         }
  469.         $photo = new Photo($this$path$isMain);
  470.         $this->photos->add($photo);
  471.         return $photo;
  472.     }
  473.     /**
  474.      * @return Photo[]
  475.      */
  476.     public function getPhotos(): Collection
  477.     {
  478.         return $this->photos->filter(function ($mediaFile): bool {
  479.             return get_class($mediaFile) == Photo::class;
  480.         });
  481.     }
  482.     public function removePhoto(string $path): bool
  483.     {
  484.         foreach ($this->getPhotos() as $photo) {
  485.             if ($path === $photo->getPath()) {
  486.                 $this->photos->removeElement($photo);
  487.                 return true;
  488.             }
  489.         }
  490.         return false;
  491.     }
  492.     public function getMainPhotoOrFirstPhoto(): ?Photo
  493.     {
  494.         $photos $this->getPhotos();
  495.         if ($photos->isEmpty()) {
  496.             return null;
  497.         }
  498.         $mainPhoto $this->getMainPhoto();
  499.         if (null === $mainPhoto) {
  500.             $mainPhoto $photos->first();
  501.         }
  502.         return $mainPhoto;
  503.     }
  504.     public function getMainPhoto(): ?Photo
  505.     {
  506.         $photos $this->getPhotos();
  507.         if ($photos->isEmpty()) {
  508.             return null;
  509.         }
  510.         $mainPhoto null;
  511.         $photos->forAll(function ($indexPhoto $photo) use (&$mainPhoto): bool {
  512.             if ($photo->isMain()) {
  513.                 $mainPhoto $photo;
  514.                 return false// Stop the cycle
  515.             }
  516.             return true;
  517.         });
  518.         return $mainPhoto;
  519.     }
  520.     public function changeMainPhoto(string $path): void
  521.     {
  522.         $photos $this->getPhotos();
  523.         $found $photos->filter(function (Photo $photo) use ($path): bool {
  524.             return $path === $photo->getPath();
  525.         });
  526.         if ($found->isEmpty()) {
  527.             return;
  528.         }
  529.         $mainPhoto $found->first();
  530.         $photos->forAll(function ($indexPhoto $photo): true {
  531.             $photo->unsetMain();
  532.             return true;
  533.         });
  534.         $mainPhoto->setMain();
  535.     }
  536.     public function addSelfie(string $path): Selfie
  537.     {
  538.         $found $this->getSelfies()->filter(function (Selfie $selfie) use ($path): bool {
  539.             return $path === $selfie->getPath();
  540.         });
  541.         if (!$found->isEmpty())
  542.             return $found->first();
  543.         $selfie = new Selfie($this$path);
  544.         $this->selfies->add($selfie);
  545.         return $selfie;
  546.     }
  547.     /**
  548.      * @return Selfie[]
  549.      */
  550.     public function getSelfies(): Collection
  551.     {
  552.         return $this->selfies;
  553.     }
  554.     public function removeSelfie(string $path): bool
  555.     {
  556.         foreach ($this->getSelfies() as $selfie) {
  557.             if ($path === $selfie->getPath()) {
  558.                 $this->selfies->removeElement($selfie);
  559.                 return true;
  560.             }
  561.         }
  562.         return false;
  563.     }
  564.     public function getConfirmedVideos(): Collection
  565.     {
  566.         return $this->videos->filter(function ($mediaFile): bool {
  567.             if (!$mediaFile instanceof Video) {
  568.                 return false;
  569.             }
  570.             return $mediaFile->isConfirmed();
  571.         });
  572.     }
  573.     /**
  574.      * Храним только 1 видео для анкеты
  575.      */
  576.     public function addVideo(string $videoPath, ?string $posterPath null): Video
  577.     {
  578.         $found $this->getVideos()->filter(function (Video $video) use ($videoPath): bool {
  579.             return $videoPath === $video->getPath();
  580.         });
  581.         if (!$found->isEmpty())
  582.             return $found->first();
  583.         $video = new Video($this$videoPath);
  584.         if (null !== $posterPath) {
  585.             $video->setPreviewPath($posterPath);
  586.         }
  587.         //теперь разрешаем много видео
  588.         //$this->videos->clear();
  589.         $this->videos->add($video);
  590.         return $video;
  591.     }
  592.     /**
  593.      * @return Video[]
  594.      */
  595.     public function getVideos(): Collection
  596.     {
  597.         return $this->videos->filter(function ($mediaFile): bool {
  598.             return ($mediaFile instanceof Video);
  599.         });
  600.     }
  601.     public function removeVideo(string $path): bool
  602.     {
  603.         foreach ($this->getVideos() as $video) {
  604.             if ($path === $video->getPath()) {
  605.                 $this->videos->removeElement($video);
  606.                 $this->photos->removeElement($video);
  607.                 return true;
  608.             }
  609.         }
  610.         return false;
  611.     }
  612.     /**
  613.      * Добавляет таск на обработку оригинала видео в подходящий формат
  614.      *
  615.      * @param string $path Путь к файлу оригинала относительно фс очередей
  616.      */
  617.     public function addRawVideo(string $path): FileProcessingTask
  618.     {
  619.         $file = new FileProcessingTask($this$path);
  620.         $this->processingFiles->add($file);
  621.         return $file;
  622.     }
  623.     public function hasFilesInProcess(): bool
  624.     {
  625.         return $this->videosInProcess() > 0;
  626.     }
  627.     public function videosInProcess(): int
  628.     {
  629.         $inProcess $this->processingFiles->filter(function (FileProcessingTask $task): bool {
  630.             return !$task->isCompleted();
  631.         });
  632.         return $inProcess->count();
  633.     }
  634.     public function isMediaProcessed(): bool
  635.     {
  636.         foreach ($this->videos as $video)
  637.             if (null === $video->getPreviewPath())
  638.                 return false;
  639.         return true;
  640.     }
  641.     public function getAvatar(): ?Avatar
  642.     {
  643.         return $this->avatar;
  644.     }
  645.     public function setAvatar(string $path): void
  646.     {
  647.         $this->avatar = new Avatar($this$path);
  648.     }
  649.     public function removeAvatar(): bool
  650.     {
  651.         if (null == $this->avatar)
  652.             return false;
  653.         foreach ($this->photos as $photo) {
  654.             if ($this->avatar->getPath() === $photo->getPath()) {
  655.                 $this->photos->removeElement($photo);
  656.                 break;
  657.             }
  658.         }
  659.         $this->avatar null;
  660.         return true;
  661.     }
  662.     /**
  663.      * @return CommentByCustomer[]
  664.      */
  665.     public function getComments(): Collection
  666.     {
  667.         return $this->comments->filter(function (CommentByCustomer $comment): bool {
  668.             return null == $comment->getParent();
  669.         });
  670.     }
  671.     /**
  672.      * @return CommentByCustomer[]
  673.      */
  674.     public function getCommentsOrderedByNotReplied(): array
  675.     {
  676.         $comments $this->comments->filter(function (CommentByCustomer $comment): bool {
  677.             return null == $comment->getParent();
  678.         })->toArray();
  679.         usort($comments, function (CommentByCustomer $commentACommentByCustomer $commentB): int {
  680.             if ((null == $commentA->getLastCommentByAdvertiser() && null == $commentB->getLastCommentByAdvertiser())
  681.                 || (null != $commentA->getLastCommentByAdvertiser() && null != $commentB->getLastCommentByAdvertiser())) {
  682.                 if ($commentA->getCreatedAt() == $commentB->getCreatedAt())
  683.                     return $commentA->getId() > $commentB->getId() ? -1;
  684.                 else
  685.                     return $commentA->getCreatedAt() > $commentB->getCreatedAt() ? -1;
  686.             }
  687.             if (null == $commentA->getLastCommentByAdvertiser() && null != $commentB->getLastCommentByAdvertiser())
  688.                 return -1;
  689.             else
  690.                 return 1;
  691.         });
  692.         return $comments;
  693.     }
  694.     public function getCreatedAt(): ?\DateTimeImmutable
  695.     {
  696.         return $this->createdAt;
  697.     }
  698.     /**
  699.      * @return CommentByCustomer[]
  700.      */
  701.     public function getCommentsWithoutReply(): Collection
  702.     {
  703.         return $this->comments->filter(function (CommentByCustomer $comment): bool {
  704.             return null == $comment->getParent() && false == $comment->isCommentedByAdvertiser();
  705.         });
  706.     }
  707.     /**
  708.      * @return CommentByCustomer[]
  709.      */
  710.     public function getCommentsWithReply(): Collection
  711.     {
  712.         return $this->comments->filter(function (CommentByCustomer $comment): bool {
  713.             return null == $comment->getParent() && true == $comment->isCommentedByAdvertiser();
  714.         });
  715.     }
  716.     /**
  717.      * @return CommentByCustomer[]
  718.      */
  719.     public function getNewComments(): Collection
  720.     {
  721.         $weekAgo CarbonImmutable::now()->sub('7 days');
  722.         return $this->comments->filter(function (CommentByCustomer $comment) use ($weekAgo): bool {
  723.             return null == $comment->getParent()
  724.                 && (
  725.                     $comment->getCreatedAt() >= $weekAgo
  726.                     || null == $this->getCommentReply($comment)
  727.                 );
  728.         });
  729.     }
  730.     private function getCommentReply(CommentByCustomer $parent): ?CommentByCustomer
  731.     {
  732.         foreach ($this->comments as $comment)
  733.             if ($comment->getParent() == $parent)
  734.                 return $comment;
  735.         return null;
  736.     }
  737.     public function getOldComments(): Collection
  738.     {
  739.         $weekAgo CarbonImmutable::now()->sub('7 days');
  740.         return $this->comments->filter(function (CommentByCustomer $comment) use ($weekAgo): bool {
  741.             return null == $comment->getParent()
  742.                 && false == (
  743.                     $comment->getCreatedAt() >= $weekAgo
  744.                     || null == $this->getCommentReply($comment)
  745.                 );
  746.         });
  747.     }
  748.     public function getCommentFromUser(Customer $user): ?CommentByCustomer
  749.     {
  750.         foreach ($this->comments as $comment)
  751.             if (null == $comment->getParent() && null != $comment->getUser() && $user->getId() == $comment->getUser()->getId())
  752.                 return $comment;
  753.         return null;
  754.     }
  755.     //TODO return type
  756.     public function getCity(): City
  757.     {
  758.         return $this->city;
  759.     }
  760.     /**
  761.      * @return Station[]
  762.      */
  763.     public function getStations(): Collection
  764.     {
  765.         return $this->stations;
  766.     }
  767.     public function getMapCoordinate(): ?MapCoordinate
  768.     {
  769.         return $this->mapCoordinate;
  770.     }
  771.     public function getUpdatedAt(): ?\DateTimeImmutable
  772.     {
  773.         return $this->updatedAt;
  774.     }
  775.     public function setUpdatedAt(\DateTimeImmutable $updatedAt): void
  776.     {
  777.         $this->updatedAt $updatedAt;
  778.     }
  779.     public function getModerationStatus(): int
  780.     {
  781.         return $this->moderationStatus;
  782.     }
  783.     public function setModerationStatus(int $status): void
  784.     {
  785.         if (self::MODERATION_STATUS_APPROVED === $status) {
  786.             throw new \RuntimeException(sprintf('Use %s::passModeration() method instead', static::class));
  787.         }
  788.         $validStatuses = [self::MODERATION_STATUS_NOT_PASSEDself::MODERATION_STATUS_APPROVEDself::MODERATION_STATUS_WAITINGself::MODERATION_STATUS_REJECTED];
  789.         if (false === array_search($status$validStatuses))
  790.             throw new \LogicException('Trying to set an invalid moderation status');
  791.         $this->moderationStatus $status;
  792.     }
  793.     public function isModerationPassed(): bool
  794.     {
  795.         return $this->moderationStatus == self::MODERATION_STATUS_APPROVED;
  796.     }
  797.     public function isModerationWaiting(): bool
  798.     {
  799.         return $this->moderationStatus == self::MODERATION_STATUS_WAITING;
  800.     }
  801.     public function isModerationRejected(): bool
  802.     {
  803.         return $this->moderationStatus == self::MODERATION_STATUS_REJECTED;
  804.     }
  805.     public function passModeration(?ModerationRequest $moderationRequest null): void
  806.     {
  807.         $this->moderationStatus self::MODERATION_STATUS_APPROVED;
  808.         $func = static function ($kPhoto|Video $file) use ($moderationRequest): bool {
  809.             if (!$file->isConfirmed()) {
  810.                 $file->passModeration($moderationRequest);
  811.             }
  812.             return true;
  813.         };
  814.         $this->videos->forAll($func);
  815.     }
  816.     public function delete(): void
  817.     {
  818.         $this->deletePlacementHiding();
  819.         $this->deleteFromAdBoard();
  820.         $now = new \DateTimeImmutable('now');
  821.         $toDelete = [];
  822.         foreach ($this->topPlacements as $topPlacement) {
  823.             if ($topPlacement->getExpiresAt() > $now)
  824.                 $toDelete[] = $topPlacement;
  825.         }
  826.         foreach ($toDelete as $topPlacement)
  827.             $this->topPlacements->removeElement($topPlacement);
  828.         $this->setDeletedAt(Carbon::now());
  829.     }
  830.     public function deletePlacementHiding(): void
  831.     {
  832.         $this->placementHiding null;
  833.     }
  834.     public function deleteFromAdBoard(): void
  835.     {
  836.         $this->adBoardPlacement null;
  837.     }
  838.     //TODO return type
  839.     public function undoDelete(): void
  840.     {
  841.         $this->setDeletedAt(); // will pass null by default
  842.     }
  843.     //TODO return type
  844.     public function deleteFromTopPlacement(): void
  845.     {
  846.         //здесь нужна логика отмены конретного размещения
  847. //        $this->topPlacement = null;
  848.     }
  849.     public function getExpressPricing(): ?ExpressPricing
  850.     {
  851.         return $this->expressPricing;
  852.     }
  853.     public function getCarPricing(): ?CarPricing
  854.     {
  855.         return $this->carPricing;
  856.     }
  857.     //TODO return type
  858.     /**
  859.      * @return int[]
  860.      */
  861.     public function getClientTypes(): array
  862.     {
  863.         return $this->clientTypes ?? [];
  864.     }
  865.     /**
  866.      * @param int[] $clientTypes
  867.      */
  868.     public function setClientTypes(array $clientTypes): void
  869.     {
  870.         $this->clientTypes $clientTypes;
  871.     }
  872.     public function getMessengers(): ?Messengers
  873.     {
  874.         return $this->messengers;
  875.     }
  876.     public function getInactivatedAt(): ?\DateTimeImmutable
  877.     {
  878.         return $this->inactivatedAt;
  879.     }
  880.     public function setInactive(): void
  881.     {
  882.         $this->inactivatedAt CarbonImmutable::now();
  883.     }
  884.     public function undoInactive(): void
  885.     {
  886.         $this->inactivatedAt null;
  887.     }
  888.     public function isHidden(): bool
  889.     {
  890.         return null !== $this->getPlacementHiding();
  891.     }
  892.     public function getPlacementHiding(): ?PlacementHiding
  893.     {
  894.         return $this->placementHiding;
  895.     }
  896.     public function setPlacementHiding(PlacementHiding $placementHiding): void
  897.     {
  898.         $this->placementHiding $placementHiding;
  899.     }
  900.     public function hasSelfie(): bool
  901.     {
  902.         return $this->selfies->count() > 0;
  903.     }
  904.     public function hasVideo(): bool
  905.     {
  906.         return $this->videos->count() > 0;
  907.     }
  908.     public function isCommented(): bool
  909.     {
  910.         return $this->comments->count() > 0;
  911.     }
  912.     public function adminApprovalPhoto(): ?AdminApprovalPhoto
  913.     {
  914.         return $this->adminApprovalPhoto;
  915.     }
  916.     public function setAdminApprovalPhoto(?string $path): void
  917.     {
  918.         $this->adminApprovalPhoto $path ? new AdminApprovalPhoto($this$path) : null;
  919.     }
  920.     public function seo(): ?array
  921.     {
  922.         return $this->seo;
  923.     }
  924.     public function seoPhoneNumber(): ?string
  925.     {
  926.         return $this->seo['phone'] ?? null;
  927.     }
  928.     public function setSeoPhoneNumber(string $phoneNumber): void
  929.     {
  930.         if (null === $this->seo) {
  931.             $this->seo = [];
  932.         }
  933.         $this->seo['phone'] = $phoneNumber;
  934.     }
  935.     public function getPrimaryStation(): ?Station
  936.     {
  937.         return $this->primaryStation;
  938.     }
  939.     public function setPrimaryStation(?Station $station): void
  940.     {
  941.         $this->primaryStation $station;
  942.         $this->normalizePrimaryStation();
  943.     }
  944.     public function getStationsSortedByPrimary(): array
  945.     {
  946.         $stations $this->stations->toArray();
  947.         if (!$this->primaryStation) {
  948.             return $stations;
  949.         }
  950.         usort($stations, function (Station $aStation $b) {
  951.             if ($a->getId() === $this->primaryStation->getId()) return -1;
  952.             if ($b->getId() === $this->primaryStation->getId()) return 1;
  953.             return 0;
  954.         });
  955.         return $stations;
  956.     }
  957. }