From 53020f827a70cf9d27030b6d912bfac6e3121d09 Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Fri, 21 Jun 2024 13:31:59 +0500 Subject: [PATCH] load & delete user image --- .gitignore | 1 + app/config/services.yaml | 12 ++ app/src/Controller/ProfileController.php | 34 +++++ app/src/Entity/User.php | 4 +- app/src/Entity/UserHistory.php | 13 ++ app/src/Entity/UserImage.php | 2 +- app/src/Listeners/UserImageListener.php | 77 ++++++++++ app/src/Listeners/UserListener.php | 82 ++++++---- .../Service/Action/Classes/DeleteImage.php | 61 ++++++++ app/src/Service/Action/Classes/SaveImage.php | 144 ++++++++++++++++++ app/src/Service/Dto/Classes/ImageDto.php | 23 +++ 11 files changed, 417 insertions(+), 36 deletions(-) create mode 100644 app/src/Listeners/UserImageListener.php create mode 100644 app/src/Service/Action/Classes/DeleteImage.php create mode 100644 app/src/Service/Action/Classes/SaveImage.php create mode 100644 app/src/Service/Dto/Classes/ImageDto.php diff --git a/.gitignore b/.gitignore index d38a7f5..647b7ba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.vscode/ .env +/app/public/uploads/ diff --git a/app/config/services.yaml b/app/config/services.yaml index cc9ab17..43d8e48 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -7,6 +7,8 @@ parameters: confirm_type: '%env(CONFIRM_TYPE)%' code_ttl: '%env(CODE_TTL)%' from_email: '%env(MAILER_ADDRESS)%' + # Директория сохранения файлов + images_directory: '%kernel.project_dir%/public/uploads/user_images' services: # default configuration for services in *this* file @@ -48,6 +50,14 @@ services: App\Service\Action\ActionServiceInterface $resetEmailService: '@App\Service\Action\Classes\ResetEmail' + App\Service\Action\ActionServiceInterface $deleteImageService: '@App\Service\Action\Classes\DeleteImage' + + App\Service\Action\ActionServiceInterface $saveImageService: '@App\Service\Action\Classes\SaveImage' + + App\Service\Action\Classes\SaveImage: + arguments: + $targetDirectory: '%images_directory%' + App\Service\Action\ActionServiceInterface: '@App\Service\Action\Classes\None' @@ -66,6 +76,8 @@ services: App\Service\Dto\DtoServiceInterface $recoveryDto: '@App\Service\Dto\Classes\RecoveryDto' + App\Service\Dto\DtoServiceInterface $imageDto: '@App\Service\Dto\Classes\ImageDto' + App\Service\Dto\DtoServiceInterface: '@App\Service\Dto\Classes\NoneDto' diff --git a/app/src/Controller/ProfileController.php b/app/src/Controller/ProfileController.php index 348eac7..dda47fc 100644 --- a/app/src/Controller/ProfileController.php +++ b/app/src/Controller/ProfileController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Service\Action\ActionServiceInterface; use App\Service\Dto\Classes\ChangeProfileDto; +use App\Service\Dto\Classes\ImageDto; use App\Service\Dto\Classes\RecoveryCodeDto; use App\Service\Dto\Classes\RecoveryDto; use App\Service\Response\Classes\ProfileResponse; @@ -116,4 +117,37 @@ class ProfileController extends AbstractController { return $resetEmailService->getResponse(); } + + #[Route('/profile/image', name: 'profile_image', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: ImageDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function saveImage( + ActionServiceInterface $saveImageService, + ): JsonResponse + { + return $saveImageService->getResponse(); + } + + #[Route('/profile/image/delete', name: 'profile_image_delete', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function deleteImage( + ActionServiceInterface $deleteImageService, + ): JsonResponse + { + return $deleteImageService->getResponse(); + } } diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php index 96c4f82..2082aa3 100644 --- a/app/src/Entity/User.php +++ b/app/src/Entity/User.php @@ -363,7 +363,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface new ReflectionExtractor() ); $serializer = new Serializer( - [$normalizer, new DateTimeNormalizer()], + [new DateTimeNormalizer(), $normalizer], [new JsonEncoder()] ); return $serializer->deserialize( @@ -388,7 +388,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface null, new ReflectionExtractor() ); - $serializer = new Serializer([$normalizer], [new JsonEncoder()]); + $serializer = new Serializer([new DateTimeNormalizer(), $normalizer], [new JsonEncoder()]); $data = $serializer->serialize($this, 'json', ['groups' => $groups]); $array = json_decode($data, true, 512, JSON_THROW_ON_ERROR); return self::createByArray($array, $groups); diff --git a/app/src/Entity/UserHistory.php b/app/src/Entity/UserHistory.php index cc4d288..887b4b9 100644 --- a/app/src/Entity/UserHistory.php +++ b/app/src/Entity/UserHistory.php @@ -129,6 +129,19 @@ class UserHistory $type = $this->getType(); switch ($field = $this->getField()) { + case 'image': + switch ($type) { + case self::TYPE_CREATE: + $text = 'Изображение загружено'; + break; + case self::TYPE_UPDATE: + $text = 'Изображение обновлено'; + break; + case self::TYPE_DELETE: + $text = 'Изображение удалено'; + break; + } + break; case 'confirm': switch ($type) { case self::TYPE_CREATE: diff --git a/app/src/Entity/UserImage.php b/app/src/Entity/UserImage.php index d58226d..0716a49 100644 --- a/app/src/Entity/UserImage.php +++ b/app/src/Entity/UserImage.php @@ -14,7 +14,7 @@ class UserImage #[ORM\Column] private ?int $id = null; - #[ORM\OneToOne(inversedBy: 'image', cascade: ['persist', 'remove'])] + #[ORM\OneToOne(inversedBy: 'image', cascade: ['persist'])] private ?User $related_user = null; #[ORM\Column(length: 255)] diff --git a/app/src/Listeners/UserImageListener.php b/app/src/Listeners/UserImageListener.php new file mode 100644 index 0000000..0eafa48 --- /dev/null +++ b/app/src/Listeners/UserImageListener.php @@ -0,0 +1,77 @@ +getObjectManager(); + if (!$this->checkFile($file)) { + $om->remove($file); + $om->flush(); + } + } + + public function preUpdate(UserImage $file, PreUpdateEventArgs $args): void + { + $om = $args->getObjectManager(); + if (!$this->checkFile($file)) { + $om->remove($file); + $om->flush(); + } + } + + public function postRemove(UserImage $file, PostRemoveEventArgs $args): void + { + $om = $args->getObjectManager(); + $this->removeFile($file); + } + + /** + * Проверка наличия файла + * + * @param UserImage $file + * + * @return bool + */ + private function checkFile(UserImage $file): bool + { + if ($path = $file->getPath()) { + $filesystem = new Filesystem(); + if ($filesystem->exists([$path])) { + return true; + } + } + + return false; + } + + /** + * Удаление файла + * + * @param UserImage $file + * + * @return void + */ + private function removeFile(UserImage $file): void + { + if ($path = $file->getPath()) { + $filesystem = new Filesystem(); + if ($filesystem->exists([$path])) { + $filesystem->remove([$path]); + } + } + } +} \ No newline at end of file diff --git a/app/src/Listeners/UserListener.php b/app/src/Listeners/UserListener.php index ac6c084..7af6d63 100644 --- a/app/src/Listeners/UserListener.php +++ b/app/src/Listeners/UserListener.php @@ -49,48 +49,64 @@ class UserListener } $checkUser = $user->newCopy(); - if ($user->getId() && $userExists) { - $reflectionClass = new ReflectionClass($user); - foreach ($reflectionClass->getProperties() as $property) { - $name = $property->getName(); - if (!isset(self::HISTORY_FIELDS[$name])) { - continue; + if ($image = $user->getImage()) { + $imageOriginalValues = $uow->getOriginalEntityData($user->getImage()); + if ($imageOriginalValues) { + if ($image->getPath() !== $imageOriginalValues['path']) { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_UPDATE); + $newUserHistory->setField('image'); + $newUserHistory->setValue($image->getName()); + $newUserHistory->setOldValue($imageOriginalValues['name']); + $user->addUserHistory($newUserHistory); } - $value = $property->getValue($checkUser); - $oldValue = $property->getValue($userExists); - if ($value !== $oldValue) { - if (empty($value) && empty($oldValue)) { + } + } + + if ($user->getId()) { + if ($userExists) { + $reflectionClass = new ReflectionClass($user); + foreach ($reflectionClass->getProperties() as $property) { + $name = $property->getName(); + if (!isset(self::HISTORY_FIELDS[$name])) { continue; } + $value = $property->getValue($checkUser); + $oldValue = $property->getValue($userExists); + if ($value !== $oldValue) { + if (empty($value) && empty($oldValue)) { + continue; + } - $type = UserHistory::TYPE_UPDATE; - if (empty($value)) { - $type = UserHistory::TYPE_DELETE; - } elseif (empty($oldValue)) { - $type = UserHistory::TYPE_CREATE; + $type = UserHistory::TYPE_UPDATE; + if (empty($value)) { + $type = UserHistory::TYPE_DELETE; + } elseif (empty($oldValue)) { + $type = UserHistory::TYPE_CREATE; + } + $newUserHistory = new UserHistory(); + $newUserHistory->setType($type); + $newUserHistory->setField($name); + $newUserHistory->setValue($value); + $newUserHistory->setOldValue($oldValue); + $user->addUserHistory($newUserHistory); } + } + if ($userExists->isDeleted() !== $user->isDeleted()) { $newUserHistory = new UserHistory(); - $newUserHistory->setType($type); - $newUserHistory->setField($name); - $newUserHistory->setValue($value); - $newUserHistory->setOldValue($oldValue); + $newUserHistory->setType($user->isDeleted() ? UserHistory::TYPE_DELETE : UserHistory::TYPE_RECOVERY); + $newUserHistory->setField('user'); $user->addUserHistory($newUserHistory); } - } - if ($userExists->isDeleted() !== $user->isDeleted()) { - $newUserHistory = new UserHistory(); - $newUserHistory->setType($user->isDeleted() ? UserHistory::TYPE_DELETE : UserHistory::TYPE_RECOVERY); - $newUserHistory->setField('user'); - $user->addUserHistory($newUserHistory); - } - if ($userExists->isConfirm() !== $user->isConfirm()) { - $newUserHistory = new UserHistory(); - $newUserHistory->setType($user->isConfirm() ? UserHistory::TYPE_CREATE : UserHistory::TYPE_DELETE); - if ($user->isConfirm()) { - $newUserHistory->setValue($user->getEmail()); + if ($userExists->isConfirm() !== $user->isConfirm()) { + $newUserHistory = new UserHistory(); + $newUserHistory->setType($user->isConfirm() ? UserHistory::TYPE_CREATE : UserHistory::TYPE_DELETE); + if ($user->isConfirm()) { + $newUserHistory->setValue($user->getEmail()); + } + $newUserHistory->setField('confirm'); + $user->addUserHistory($newUserHistory); } - $newUserHistory->setField('confirm'); - $user->addUserHistory($newUserHistory); } } else { $newUserHistory = new UserHistory(); diff --git a/app/src/Service/Action/Classes/DeleteImage.php b/app/src/Service/Action/Classes/DeleteImage.php new file mode 100644 index 0000000..6431f4f --- /dev/null +++ b/app/src/Service/Action/Classes/DeleteImage.php @@ -0,0 +1,61 @@ +user = $security->getUser(); + parent::__construct($response); + } + + public function runAction(): void + { + $image = $this->user->getImage(); + if ($image) { + $em = $this->doctrine->getManager(); + + try { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_DELETE); + $newUserHistory->setField('image'); + $newUserHistory->setValue($image->getName()); + $this->user->addUserHistory($newUserHistory); + $em->remove($image); + $em->flush(); + $this->response->addMessage('Изображение удалено'); + } catch (\Exception $exception) { + $this->response->addError('Ошибка удаления изображения'); + } + } else { + $this->response->addError('Нет изображения'); + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->addError('Вы не авторизованы'); + return false; + } + if ($this->user->isDeleted()) { + $this->response->addError('Профиль удален'); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/SaveImage.php b/app/src/Service/Action/Classes/SaveImage.php new file mode 100644 index 0000000..4e437de --- /dev/null +++ b/app/src/Service/Action/Classes/SaveImage.php @@ -0,0 +1,144 @@ +user = $security->getUser(); + parent::__construct($response); + } + + public function runAction(): void + { + $file = $this->saveFile(); + if ($file) { + $em = $this->doctrine->getManager(); + + $oldImage = $this->user->getImage(); + if ($oldImage) { + $oldImage->setName($file->getName()); + $oldImage->setType($file->getType()); + $oldImage->setPath($file->getPath()); + $file = $oldImage; + } else { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_CREATE); + $newUserHistory->setField('image'); + $newUserHistory->setValue($file->getName()); + $this->user->addUserHistory($newUserHistory); + $this->user->setImage($file); + } + try { + $em->persist($file); + $em->flush(); + $this->response->addMessage('Изображение сохранено'); + } catch (\Exception $exception) { + $this->response->addError('Ошибка сохранения файла пользователя'); + } + } else { + $this->response->addError('Ошибка сохранения файла'); + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->addError('Вы не авторизованы'); + return false; + } + if ($this->user->isDeleted()) { + $this->response->addError('Профиль удален'); + return false; + } + return $this->imageDto->validate($this->response); + } + + public function saveFile(): ?UserImage + { + /** @var ImageDto $dto */ + $dto = $this->imageDto->getClass(); + + $matches = []; + if (!preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,([a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$)/i', $dto->data ?: '', $matches)) { + return null; + } + $extension = $matches[1]; + $content = $matches[4]; + if ($extension && $content) { + $mimeTypes = new MimeTypes(); + $types = $mimeTypes->getExtensions($extension); + if (empty($types)) { + $this->response->addError('Неизвестное расширения файла'); + return null; + } + if (empty(array_intersect($types, self::IMAGE_EXTENSIONS))) { + $this->response->addError('Файл расширения "'. reset($types) .'" недоступен для загрузки. Доступные расширения: ' . implode(', ', self::IMAGE_EXTENSIONS) . '.'); + return null; + } + + $filename = pathinfo($dto->name, PATHINFO_FILENAME); + + $filename = $filename . '.' . reset($types); + + $decoded = base64_decode($content); + if (!$decoded) { + $this->response->addError('Ошибка декодирования файла'); + return null; + } + + $tmpPath = sys_get_temp_dir() . '/file_upload' . uniqid(); + + if (file_put_contents($tmpPath, base64_decode($content))) { + $uploadFile = new UploadedFile($tmpPath, $filename, $extension, null, true); + + $originalFilename = pathinfo($uploadFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $this->slugger->slug($originalFilename); + $fileName = $safeFilename . '-' . uniqid() . '.' . $uploadFile->guessExtension(); + $filedir = $this->targetDirectory; + try { + $file = $uploadFile->move($filedir, $fileName); + $dmFile = new UserImage(); + $dmFile->setName($uploadFile->getClientOriginalName()); + $dmFile->setPath($file->getRealPath()); + $dmFile->setType($extension); + return $dmFile; + } catch (FileException $e) { + return null; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/Classes/ImageDto.php b/app/src/Service/Dto/Classes/ImageDto.php new file mode 100644 index 0000000..5f47f0d --- /dev/null +++ b/app/src/Service/Dto/Classes/ImageDto.php @@ -0,0 +1,23 @@ +