Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion backend/app/DomainObjects/ImageDomainObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

namespace HiEvents\DomainObjects;

class ImageDomainObject extends Generated\ImageDomainObjectAbstract
use HiEvents\DomainObjects\Interfaces\IsSortable;
use HiEvents\DomainObjects\SortingAndFiltering\AllowedSorts;

class ImageDomainObject extends Generated\ImageDomainObjectAbstract implements IsSortable
{
public static function getAllowedSorts(): AllowedSorts
{
return new AllowedSorts(
[
self::CREATED_AT => [
'asc' => __('Oldest First'),
'desc' => __('Newest First'),
],
self::FILENAME => [
'asc' => __('Filename A-Z'),
'desc' => __('Filename Z-A'),
],
],
);
}

public static function getDefaultSort(): string
{
return self::CREATED_AT;
}

public static function getDefaultSortDirection(): string
{
return 'desc';
}
}
20 changes: 20 additions & 0 deletions backend/app/Exceptions/ImageInUseException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace HiEvents\Exceptions;

use Exception;
use HiEvents\Services\Domain\Image\DTO\ImageReferenceDTO;

class ImageInUseException extends Exception
{
/**
* @param ImageReferenceDTO[] $references
*/
public function __construct(
public readonly array $references,
public readonly bool $hasProtectedReferences,
string $message = '',
) {
parent::__construct($message ?: 'Image is referenced by other content', 409);
}
}
32 changes: 23 additions & 9 deletions backend/app/Http/Actions/Images/DeleteImageAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,45 @@

use HiEvents\DomainObjects\ImageDomainObject;
use HiEvents\Exceptions\CannotDeleteEntityException;
use HiEvents\Exceptions\ImageInUseException;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Http\ResponseCodes;
use HiEvents\Services\Application\Handlers\Images\DeleteImageHandler;
use HiEvents\Services\Application\Handlers\Images\DTO\DeleteImageDTO;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class DeleteImageAction extends BaseAction
{
public function __construct(
public readonly DeleteImageHandler $deleteImageHandler,
)
{
}
) {}

/**
* @throws CannotDeleteEntityException
*/
public function __invoke(int $imageId): Response
public function __invoke(int $imageId, Request $request): JsonResponse|Response
{
$this->isActionAuthorized($imageId, ImageDomainObject::class);

$this->deleteImageHandler->handle(new DeleteImageDTO(
imageId: $imageId,
userId: $this->getAuthenticatedUser()->getId(),
accountId: $this->getAuthenticatedAccountId(),
));
$confirm = $request->boolean('confirm');

try {
$this->deleteImageHandler->handle(new DeleteImageDTO(
imageId: $imageId,
userId: $this->getAuthenticatedUser()->getId(),
accountId: $this->getAuthenticatedAccountId(),
confirm: $confirm,
));
} catch (ImageInUseException $exception) {
return $this->jsonResponse([
'message' => $exception->getMessage(),
'has_protected_references' => $exception->hasProtectedReferences,
'deletable_with_confirm' => ! $exception->hasProtectedReferences,
'references' => array_map(static fn ($r) => $r->toArray(), $exception->references),
], ResponseCodes::HTTP_CONFLICT);
}

return $this->noContentResponse();
}
Expand Down
37 changes: 37 additions & 0 deletions backend/app/Http/Actions/Images/GetAccountImagesAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace HiEvents\Http\Actions\Images;

use HiEvents\DomainObjects\AccountDomainObject;
use HiEvents\DomainObjects\ImageDomainObject;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Http\DTO\QueryParamsDTO;
use HiEvents\Repository\Interfaces\ImageRepositoryInterface;
use HiEvents\Resources\Image\ImageResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class GetAccountImagesAction extends BaseAction
{
public function __construct(private readonly ImageRepositoryInterface $imageRepository)
{
}

public function __invoke(Request $request): JsonResponse
{
$accountId = $this->getAuthenticatedAccountId();

$this->isActionAuthorized($accountId, AccountDomainObject::class);

$images = $this->imageRepository->findByAccountId(
$accountId,
QueryParamsDTO::fromArray($request->query->all()),
);

return $this->filterableResourceResponse(
resource: ImageResource::class,
data: $images,
domainObject: ImageDomainObject::class,
);
}
}
28 changes: 28 additions & 0 deletions backend/app/Repository/Eloquent/ImageRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

namespace HiEvents\Repository\Eloquent;

use HiEvents\DomainObjects\Generated\ImageDomainObjectAbstract;
use HiEvents\DomainObjects\ImageDomainObject;
use HiEvents\Http\DTO\QueryParamsDTO;
use HiEvents\Models\Image;
use HiEvents\Repository\Interfaces\ImageRepositoryInterface;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;

/**
* @extends BaseRepository<ImageDomainObject>
Expand All @@ -20,4 +24,28 @@ public function getDomainObject(): string
{
return ImageDomainObject::class;
}

public function findByAccountId(int $accountId, QueryParamsDTO $params): LengthAwarePaginator
{
$where = [
[ImageDomainObjectAbstract::ACCOUNT_ID, '=', $accountId],
];

if ($params->query) {
$where[] = static function (Builder $builder) use ($params) {
$builder->where(ImageDomainObjectAbstract::FILENAME, 'ilike', '%' . $params->query . '%');
};
}

$this->model = $this->model->orderBy(
column: $this->validateSortColumn($params->sort_by, ImageDomainObject::class),
direction: $this->validateSortDirection($params->sort_direction, ImageDomainObject::class),
);

return $this->paginateWhere(
where: $where,
limit: $params->per_page,
page: $params->page,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
namespace HiEvents\Repository\Interfaces;

use HiEvents\DomainObjects\ImageDomainObject;
use HiEvents\Http\DTO\QueryParamsDTO;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

/**
* @extends RepositoryInterface<ImageDomainObject>
*/
interface ImageRepositoryInterface extends RepositoryInterface
{

public function findByAccountId(int $accountId, QueryParamsDTO $params): LengthAwarePaginator;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
use HiEvents\Repository\Interfaces\ImageRepositoryInterface;
use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface;
use HiEvents\Services\Application\Handlers\Images\DTO\CreateImageDTO;
use HiEvents\Services\Domain\Image\ImageInUseService;
use HiEvents\Services\Domain\Image\ImageUploadService;
use HiEvents\Services\Infrastructure\Image\Exception\CouldNotUploadImageException;
use HiEvents\Services\Infrastructure\Image\ImageStorageService;

class CreateImageHandler
{
Expand All @@ -24,13 +26,13 @@ class CreateImageHandler
];

public function __construct(
private readonly ImageUploadService $imageUploadService,
private readonly ImageUploadService $imageUploadService,
private readonly OrganizerRepositoryInterface $organizerRepository,
private readonly EventRepositoryInterface $eventRepository,
private readonly ImageRepositoryInterface $imageRepository,
)
{
}
private readonly EventRepositoryInterface $eventRepository,
private readonly ImageRepositoryInterface $imageRepository,
private readonly ImageStorageService $imageStorageService,
private readonly ImageInUseService $imageInUseService,
) {}

/**
* @throws CouldNotUploadImageException
Expand Down Expand Up @@ -91,12 +93,35 @@ private function validateEntityBelongsToUser(int $accountId, int $entityId, stri

private function deleteExistingImages(CreateImageDTO $imageData, string $entityType): void
{
if (in_array($imageData->imageType, self::IMAGES_TYPES_WITH_ONLY_ONE_IMAGE_ALLOWED, true)) {
$this->imageRepository->deleteWhere([
'entity_id' => $imageData->entityId,
'entity_type' => $entityType,
'type' => $imageData->imageType->name,
]);
if (! in_array($imageData->imageType, self::IMAGES_TYPES_WITH_ONLY_ONE_IMAGE_ALLOWED, true)) {
return;
}

$where = [
'entity_id' => $imageData->entityId,
'entity_type' => $entityType,
'type' => $imageData->imageType->name,
];

$existing = $this->imageRepository->findWhere($where);

$this->imageRepository->deleteWhere($where);

foreach ($existing as $image) {
if ($image->getDisk() === null || $image->getPath() === null) {
continue;
}

$references = $this->imageInUseService->findReferences(
$image->getPath(),
$imageData->accountId,
);

if (! empty($references)) {
continue;
}

$this->imageStorageService->delete($image->getDisk(), $image->getPath());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ public function __construct(
public readonly int $imageId,
public readonly int $userId,
public readonly int $accountId,
)
{
}
public readonly bool $confirm = false,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@

use HiEvents\DomainObjects\ImageDomainObject;
use HiEvents\Exceptions\CannotDeleteEntityException;
use HiEvents\Exceptions\ImageInUseException;
use HiEvents\Repository\Interfaces\ImageRepositoryInterface;
use HiEvents\Services\Application\Handlers\Images\DTO\DeleteImageDTO;
use HiEvents\Services\Domain\Image\ImageInUseService;
use HiEvents\Services\Infrastructure\Image\ImageStorageService;

class DeleteImageHandler
{
public function __construct(
private readonly ImageRepositoryInterface $imageRepository,
)
{
}
private readonly ImageStorageService $imageStorageService,
private readonly ImageInUseService $imageInUseService,
) {}

/**
* @throws CannotDeleteEntityException
* @throws ImageInUseException
*/
public function handle(DeleteImageDTO $imageData): void
{
Expand All @@ -30,9 +34,41 @@ public function handle(DeleteImageDTO $imageData): void
throw new CannotDeleteEntityException('You do not have permission to delete this image.');
}

$references = $image->getPath() !== null
? $this->imageInUseService->findReferences($image->getPath(), $imageData->accountId)
: [];

$hasProtected = false;
foreach ($references as $ref) {
if ($ref->is_protected) {
$hasProtected = true;
break;
}
}

if ($hasProtected) {
throw new ImageInUseException(
references: $references,
hasProtectedReferences: true,
message: __('Image is referenced by an active event and cannot be deleted.'),
);
}

if (! empty($references) && ! $imageData->confirm) {
throw new ImageInUseException(
references: $references,
hasProtectedReferences: false,
message: __('Image is referenced by other content. Confirm to delete anyway.'),
);
}

$this->imageRepository->deleteWhere([
'id' => $imageData->imageId,
'account_id' => $imageData->accountId,
]);

if ($image->getDisk() !== null && $image->getPath() !== null) {
$this->imageStorageService->delete($image->getDisk(), $image->getPath());
}
}
}
18 changes: 18 additions & 0 deletions backend/app/Services/Domain/Image/DTO/ImageReferenceDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace HiEvents\Services\Domain\Image\DTO;

use HiEvents\DataTransferObjects\BaseDataObject;

class ImageReferenceDTO extends BaseDataObject
{
public function __construct(
public readonly string $entity_type,
public readonly int $entity_id,
public readonly string $entity_name,
public readonly string $field_label,
public readonly bool $is_protected,
public readonly ?string $event_status = null,
public readonly ?string $event_lifecycle_status = null,
) {}
}
Loading
Loading