Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
ChangeTripImageHandler
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
2 / 2
3
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Trip\Application\Command;
6
7use App\Shared\Infrastructure\Storage\ImageStorage;
8use App\Shared\Infrastructure\Storage\SignedImageUrl;
9use App\Trip\Application\Query\TripView;
10use App\Trip\Application\TripAccess;
11use App\Trip\Domain\TripRepository;
12use App\Trip\Domain\ValueObject\TripId;
13use App\Trip\Domain\ValueObject\TripRole;
14
15/**
16 * ponytail: depends on the concrete ImageStorage rather than a port. It is a
17 * shared infrastructure utility (like a filesystem), there is one implementation,
18 * and keeping the store/unlink pair here is what makes "one image per trip, no
19 * orphans" true by construction. Introduce a port the day storage moves off disk.
20 */
21final readonly class ChangeTripImageHandler
22{
23    public function __construct(
24        private TripAccess $access,
25        private TripRepository $trips,
26        private ImageStorage $storage,
27        private SignedImageUrl $imageUrls,
28    ) {
29    }
30
31    public function handle(ChangeTripImageCommand $command): TripView
32    {
33        // Owner only: a stranger gets TripNotFound (404), a non-owner member
34        // TripAccessDenied (403). Runs before the file is stored.
35        $trip = $this->access->getForManage(new TripId($command->tripId), $command->userId);
36
37        $previous = $trip->imageName();
38        $trip->changeImage($command->file === null ? null : $this->storage->store($command->file));
39        $this->trips->save($trip);
40
41        // Only once the new name is persisted, so a failure never leaves the trip
42        // pointing at a file that is already gone.
43        $this->storage->delete($previous);
44
45        return TripView::fromTrip($trip, TripRole::OWNER, $this->imageUrls->for($trip->imageName()));
46    }
47}