src/Service/Content/ImagesetService.php line 116

Open in your IDE?
  1. <?php
  2. namespace App\Service\Content;
  3. use App\Dictionary\ContentStatus;
  4. use App\Dictionary\ContentType;
  5. use App\Dictionary\ImageFilter;
  6. use App\Entity\Category;
  7. use App\Entity\Content;
  8. use App\Entity\ContentImageset;
  9. use App\Entity\ContentImagesetImage;
  10. use App\Entity\Member;
  11. use App\Entity\MemberContent;
  12. use App\Event\Content\ImagesetImageDeleteEvent;
  13. use App\Exception\Content\ContentStatusException;
  14. use App\Lib\Content\Imagine\PixelateFilter;
  15. use App\Lib\Content\PreviewPhoto;
  16. use App\Repository\ContentImagesetImageRepository;
  17. use App\Repository\ContentImagesetRepository;
  18. use App\Response\EntityRemoved;
  19. use App\Service\Media\FileUploader\ImagesetImageUploader;
  20. use App\Service\Media\StorageLayer;
  21. use Doctrine\ORM\Tools\Pagination\Paginator;
  22. use Imagine\Image\Box;
  23. use Imagine\Image\ImageInterface;
  24. use Imagine\Image\ImagineInterface;
  25. use Imagine\Image\ManipulatorInterface;
  26. use League\Flysystem\FileExistsException;
  27. use League\Flysystem\FileNotFoundException;
  28. use Liip\ImagineBundle\Service\FilterService;
  29. use Psr\Log\LoggerInterface;
  30. use Symfony\Component\HttpFoundation\FileBag;
  31. use Symfony\Component\HttpFoundation\ParameterBag;
  32. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  33. class ImagesetService extends AbstractContentService
  34. {
  35.     /**
  36.      * @var string
  37.      */
  38.     public const SECRET_KEY 'KdkjFdZj3oijiejopJWjp93jIJouh34KPol3B';
  39.     /**
  40.      * @var ContentImagesetRepository
  41.      */
  42.     protected $repo;
  43.     protected ContentImagesetImageRepository $imageRepo;
  44.     protected ImagesetImageUploader $uploader;
  45.     protected ContentService $contentService;
  46.     protected FilterService $filterService;
  47.     protected ImagineInterface $imagine;
  48.     protected LoggerInterface $logger;
  49.     protected StorageLayer $storageLayer;
  50.     protected PixelateFilter $pixelateFilter;
  51.     /**
  52.      * ImagesetService constructor.
  53.      */
  54.     public function __construct(ContentImagesetRepository $repoContentImagesetImageRepository $imageRepository,
  55.         ImagesetImageUploader $uploader,
  56.         ContentService $contentServiceFilterService $filterService,
  57.         EventDispatcherInterface $dispatcherImagineInterface $imagine,
  58.         LoggerInterface $loggerStorageLayer $storageLayer)
  59.     {
  60.         parent::__construct($repo$dispatcher);
  61.         $this->logger $logger;
  62.         $this->contentService $contentService;
  63.         $this->filterService $filterService;
  64.         $this->imagine $imagine;
  65.         $this->imageRepo $imageRepository;
  66.         $this->storageLayer $storageLayer;
  67.         $this->setUploader($uploader);
  68.         $this->pixelateFilter = new PixelateFilter($this->imagine);
  69.     }
  70.     public function getImagesetRepo(): ContentImagesetRepository
  71.     {
  72.         return $this->repo;
  73.     }
  74.     public function getUploader(): ImagesetImageUploader
  75.     {
  76.         return $this->uploader;
  77.     }
  78.     public function setUploader(ImagesetImageUploader $uploader): self
  79.     {
  80.         $this->uploader $uploader;
  81.         return $this;
  82.     }
  83.     /**
  84.      * @throws \Doctrine\ORM\ORMException
  85.      * @throws \Doctrine\ORM\OptimisticLockException
  86.      */
  87.     public function storeContentImageset(ContentImageset $entity): ContentImageset
  88.     {
  89.         $entity->setPics($entity->getImages()->count());
  90.         return $this->storeEntity($entity);
  91.     }
  92.     public function findBySlug(string $slugbool $ignoreContentStatus false): ?ContentImageset
  93.     {
  94.         if ($ignoreContentStatus) {
  95.             return $this->getRepository()->findOneBy(['slug' => $slug]);
  96.         }
  97.         $qb $this->getRepository()->getBaseImagesetQueryBuilder([]);
  98.         $qb->andWhere('i.slug = :slug');
  99.         $qb->setParameter(':slug'$slug);
  100.         return $qb->getQuery()->getOneOrNullResult();
  101.     }
  102.     public function findByPublicPath(string $publicPath): ?ContentImageset
  103.     {
  104.         return $this->getRepository()->findOneBy(['public_path' => $publicPath]);
  105.     }
  106.     public function mayAddImagesToContentImageset(ContentImageset $imageset): bool
  107.     {
  108.         $content $imageset->getContent();
  109.         if (ContentType::IMAGESET !== $content->getType()) {
  110.             return false;
  111.         }
  112.         return ContentStatus::ACTIVE !== $content->getStatus();
  113.     }
  114.     public function createImagesetImageForSet(ContentImageset $contentImageset): ContentImagesetImage
  115.     {
  116.         if (!$this->mayAddImagesToContentImageset($contentImageset)) {
  117.             throw new ContentStatusException('Content already published.');
  118.         }
  119.         $image = new ContentImagesetImage();
  120.         $contentImageset->addImagesetImage($image);
  121.         return $image;
  122.     }
  123.     public function applyValuesToImageEntity(ParameterBag $bagFileBag $fileBagContentImagesetImage $entity): ContentImagesetImage
  124.     {
  125.         try {
  126.             $defaultPriority $entity->getContentImageset()->getImages()->count() + 1;
  127.         } catch (\Throwable $t) {
  128.             $defaultPriority 1;
  129.         }
  130.         $entity->setPriority($bag->getInt('priority'$defaultPriority));
  131.         if ($fileBag->has('file')) {
  132.             $fileName $this->getUploader()->upload($fileBag->get('file'));
  133.             if (null === $fileName) {
  134.                 throw new \Exception('writing file failed');
  135.             }
  136.         }
  137.         return $entity;
  138.     }
  139.     /**
  140.      * @throws \Doctrine\ORM\ORMException
  141.      * @throws \Doctrine\ORM\OptimisticLockException
  142.      */
  143.     public function deleteImagesetImage(ContentImagesetImage $image): EntityRemoved
  144.     {
  145.         // deletes files
  146.         $this->getEventDispatcher()->dispatch(new ImagesetImageDeleteEvent($image));
  147.         return new EntityRemoved($this->imageRepo->deleteEntity($image->getId(), true), $image->getId());
  148.     }
  149.     public function deleteImageFiltersInStorage(ContentImagesetImage $image, array $filters = [], ?bool $blur null): void
  150.     {
  151.         if (empty($filters)) {
  152.             $filters = [ImageFilter::PREVIMG_SMALLImageFilter::PREVIMG_BIGImageFilter::PHOTOSImageFilter::THUMBS];
  153.         }
  154.         $filesToDelete = [];
  155.         foreach ($filters as $filter) {
  156.             if (null === $blur) {
  157.                 $url $this->getUrlForImagesetImage($imagefalse$filter);
  158.                 $filesToDelete[] = substr($urlstrpos($url'/media/'));
  159.                 $url $this->getUrlForImagesetImage($imagetrue$filter);
  160.                 $filesToDelete[] = substr($urlstrpos($url'/media/'));
  161.             } else {
  162.                 $url $this->getUrlForImagesetImage($image$blur$filter);
  163.                 $filesToDelete[] = substr($urlstrpos($url'/media/'));
  164.             }
  165.         }
  166.         foreach ($filesToDelete as $filepath) {
  167.             if (empty($filepath)) {
  168.                 continue;
  169.             }
  170.             try {
  171.                 $this->storageLayer->getMediacacheStorage()->delete($filepath);
  172.             } catch (FileNotFoundException $e) {
  173.                 $this->logger->warning('Not found when deleting imageset image: '.$e->getMessage().PHP_EOL.$e->getTraceAsString());
  174.             }
  175.         }
  176.     }
  177.     public function getImagesetImageById(int $id): ?ContentImagesetImage
  178.     {
  179.         return $this->imageRepo->find($id);
  180.     }
  181.     /**
  182.      * @return ContentImagesetImage[]
  183.      */
  184.     public function getImagesetImagesById(array $ids): array
  185.     {
  186.         return $this->imageRepo->findBy(['id' => $ids]);
  187.     }
  188.     public function getImagesetImagesByMemberContent(array $memberContents): array
  189.     {
  190.         $finalImages = [];
  191.         $imagesetImageIds = [];
  192.         /** @var MemberContent $mc */
  193.         foreach ($memberContents as $mc) {
  194.             $imageId $mc->getImagesetImageId();
  195.             // Add all images for old memberContent where imagesetImageId is null
  196.             if (null === $imageId) {
  197.                 $allImages $mc->getContent()->getImageset()->getImages();
  198.                 foreach ($allImages as $image) {
  199.                     $finalImages[$image->getId()] = $image;
  200.                 }
  201.                 continue;
  202.             }
  203.             // Add single image purchase
  204.             if (!isset($finalImages[$imageId])) {
  205.                 $imagesetImageIds[$mc->getImagesetImageId()] = $mc->getImagesetImageId();
  206.             }
  207.         }
  208.         return array_merge($finalImages$this->getImagesetImagesById($imagesetImageIds));
  209.     }
  210.     public function getImagesetImageByPosition(ContentImageset $imagesetint $position): ?ContentImagesetImage
  211.     {
  212.         return $this->imageRepo->findOneBy([
  213.             'content_imageset' => $imageset,
  214.             'priority' => $position,
  215.         ]);
  216.     }
  217.     public function getActiveImagesetsByCategory(Category $categoryint $pageint $limit): Paginator
  218.     {
  219.         $qb $this->repo->getActiveImagesetsByCategoryQueryBuilder($category);
  220.         $qb->setMaxResults($limit);
  221.         $qb->setFirstResult($page $limit $limit);
  222.         return new Paginator($qb->getQuery());
  223.     }
  224.     public function getImagesetsByMember(Member $memberint $page, array $statusesint $limit): Paginator
  225.     {
  226.         return $this->repo->getImagesetsByMember($member$page$statuses$limit);
  227.     }
  228.     public function getFilenameHash(Content $contentContentImagesetImage $imagebool $blur false): string
  229.     {
  230.         $secret $this->getSecret($content);
  231.         $hash2 md5($content->getId().'/'.$image->getId().$secret.($blur 'A' 'B'));
  232.         return substr($hash208);
  233.     }
  234.     protected function getSecret(Content $content): string
  235.     {
  236.         return self::SECRET_KEY.$content->getCreatedAt()->format('Ym');
  237.     }
  238.     public function getFoldernameHash(Content $content): string
  239.     {
  240.         $hash md5($content->getId().$this->getSecret($content));
  241.         return substr($hash, -2).'/'.substr($hash, -42).'/'.$content->getId();
  242.     }
  243.     /**
  244.      * @return string
  245.      */
  246.     public function getFullHash(Content $contentContentImagesetImage $imagebool $blur)
  247.     {
  248.         return $this->getFoldernameHash($content).'/'.$image->getId().'.'.$this->getFilenameHash($content$image$blur);
  249.     }
  250.     public function getSourceFile(int $contentint $image): string
  251.     {
  252.         return $this->getSourceFilepath($content).'/'.$this->getSourceFilename($image);
  253.     }
  254.     public function getSourceFilename(int $imageId): string
  255.     {
  256.         return $imageId.'.large.jpg';
  257.     }
  258.     public function getSourceFilepath(int $content): string
  259.     {
  260.         $paddedId str_pad($content7'0'STR_PAD_LEFT);
  261.         $parts = [
  262.             substr($paddedId02),
  263.             substr($paddedId22),
  264.             substr($paddedId42),
  265.             $paddedId,
  266.         ];
  267.         return implode('/'$parts);
  268.     }
  269.     public function makeHash(Content $contentContentImagesetImage $imagebool $blur false)
  270.     {
  271.         $secret $this->getSecret($content);
  272.         $hash2 md5($content->getId().'/'.$image->getId().$secret.($blur 'A' 'B'));
  273.         return substr($hash208);
  274.     }
  275.     public function getUrlForImagesetImage(ContentImagesetImage $imagebool $blurstring $filter 'previmg_big'): ?string
  276.     {
  277.         $path $this->getPathForUrl($image$blur);
  278.         if (null === $path || ($blur && ImageFilter::PHOTOS === $filter)) {
  279.             return null;
  280.         }
  281.         return sprintf('%s/mediacache/media/%s/%s'$_ENV['APP_HOST_ASSETS'], $filter$path);
  282.     }
  283.     /**
  284.      * Will always overwrite blurred file.
  285.      */
  286.     public function getUrlForImagesetImageWithProcessing(ContentImagesetImage $imagesetImagebool $blurstring $filterbool $deleteBeforeGenerate false): ?string
  287.     {
  288.         $path $this->getPathForUrl($imagesetImage$blur);
  289.         if (null === $path || ($blur && ImageFilter::PHOTOS === $filter)) {
  290.             return null;
  291.         }
  292.         try {
  293.             if ($deleteBeforeGenerate) {
  294.                 try {
  295.                     $this->storageLayer->getMediacacheStorage()->delete('/media/'.$filter.'/'.$path);
  296.                 } catch (FileNotFoundException $fee) {
  297.                     $this->logger->debug('info: '.$fee->getMessage().PHP_EOL.$fee->getTraceAsString());
  298.                 }
  299.             }
  300.             \Imagick::setResourceLimit(\Imagick::RESOURCETYPE_THREAD8);
  301.             // getUrlOfFilteredImage() triggers liip-imagine filters
  302.             $url $this->filterService->getUrlOfFilteredImage($path$filter);
  303.             // Photos-filter does not need blurring and thumbs is already done by ImagesetThumbnailDataLoader.
  304.             if ($blur && in_array($filter, [ImageFilter::PREVIMG_SMALLImageFilter::PREVIMG_BIG], true)) {
  305.                 $readPath sprintf('/media/%s/%s'$filter$path);
  306.                 $resource $this->storageLayer->getMediacacheStorage()->readStream($readPath);
  307.                 $image $this->imagine->read($resource);
  308.                 $image $this->pixelateImagesetImage($image);
  309.                 // Always overwrite blurred file
  310.                 try {
  311.                     $this->storageLayer->getMediacacheStorage()->write($readPath$image->get('webp'));
  312.                 } catch (FileExistsException $fee) {
  313.                     $this->storageLayer->getMediacacheStorage()->update($readPath$image->get('webp'));
  314.                 }
  315.             }
  316.             return $url;
  317.         } catch (\Exception $e) {
  318.             $this->logger->error('1st msg: '.$e->getMessage().' 2nd code: '.$e->getCode().PHP_EOL.$e->getTraceAsString());
  319.             $this->logger->error('2nd msg: '.$e->getPrevious()?->getMessage().' 2nd code: '.$e->getPrevious()?->getCode().PHP_EOL);
  320.         }
  321.         return null;
  322.     }
  323.     private function pixelateImagesetImage(ImageInterface $image): ImageInterface
  324.     {
  325.         return $this->pixelateFilter->applyForImagesetImage($image);
  326.     }
  327.     private function getPathForUrl(ContentImagesetImage $imagebool $blur): ?string
  328.     {
  329.         $set $image->getContentImageset();
  330.         $hash $this->makeHash($set->getContent(), $image$blur);
  331.         $path $set->getPublicPath();
  332.         if (empty($path)) {
  333.             return null;
  334.         }
  335.         return $path.'/'.$image->getId().'.'.$hash.'.jpg';
  336.     }
  337.     public function bustCacheForImagesetImage(ContentImagesetImage $imagestring $filter): bool
  338.     {
  339.         $set $image->getContentImageset();
  340.         $path $this->getFoldernameHash($set->getContent());
  341.         // blurred
  342.         $hash $this->getFilenameHash($set->getContent(), $imagetrue);
  343.         $filename $image->getId().'.'.$hash.'.jpg';
  344.         $this->filterService->bustCache($path.'/'.$filename$filter);
  345.         // original
  346.         $hash $this->getFilenameHash($set->getContent(), $imagefalse);
  347.         $filename $image->getId().'.'.$hash.'.jpg';
  348.         $this->filterService->bustCache($path.'/'.$filename$filter);
  349.         return true;
  350.     }
  351.     /**
  352.      * @throws FileExistsException
  353.      * @throws FileNotFoundException
  354.      */
  355.     public function resizePreviewPhoto(PreviewPhoto $photo): bool
  356.     {
  357.         $path $photo->getStoragePath();
  358.         if (!$this->storageLayer->hasPublicFile($path)) {
  359.             return false;
  360.         }
  361.         $input $this->storageLayer->readPublicFileStream($path);
  362.         $image $this->imagine->read($input);
  363.         if ($image->getSize()->getWidth() <= 640 && $image->getSize()->getHeight() < 360) {
  364.             return false;
  365.         }
  366.         $size = new Box(640360);
  367.         $thumb $image->thumbnail($sizeManipulatorInterface::THUMBNAIL_OUTBOUND);
  368.         $this->storageLayer->overwritePublicFile($path$thumb->get('png'));
  369.         return true;
  370.     }
  371.     public function getImagesetImageIdByWebPath(string $path): int
  372.     {
  373.         return (int) explode('.'basename($path))[0];
  374.     }
  375.     public function getImagesetByWebpath(string $path): ContentImageset
  376.     {
  377.         if (in_array(substr($path, -4), ['.jpg''.png']) || in_array(substr($path, -5), ['.jpeg''.webp'])) {
  378.             $path dirname($path);
  379.         }
  380.         return $this->findByPublicPath($path);
  381.     }
  382. }