Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
SignInWithGoogleHandler
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
2 / 2
5
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%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace App\Identity\Application\Command;
6
7use App\Identity\Application\Port\GoogleIdTokenVerifier;
8use App\Identity\Domain\User;
9use App\Identity\Domain\UserIdentity;
10use App\Identity\Domain\UserIdentityRepository;
11use App\Identity\Domain\UserRepository;
12use App\Identity\Domain\ValueObject\IdentityProvider;
13use App\Identity\Domain\ValueObject\UserId;
14use App\Shared\Domain\ValueObject\Currency;
15
16final readonly class SignInWithGoogleHandler
17{
18    /** Auto-provisioned accounts start here; the user changes it in their profile. */
19    private const DEFAULT_CURRENCY = 'EUR';
20
21    public function __construct(
22        private GoogleIdTokenVerifier $verifier,
23        private UserRepository $users,
24        private UserIdentityRepository $identities,
25    ) {
26    }
27
28    /**
29     * Returns the signed-in user, or null when the token does not verify. The
30     * aggregate rather than its id: the caller builds a SecurityUser out of it,
31     * so returning the id would only buy a second query.
32     */
33    public function handle(SignInWithGoogleCommand $command): ?User
34    {
35        $identity = $this->verifier->verify($command->idToken);
36
37        if ($identity === null) {
38            return null;
39        }
40
41        $known = $this->identities->ofProviderSubject(IdentityProvider::GOOGLE, $identity->subject);
42
43        if ($known !== null) {
44            return $this->users->ofId($known->userId());
45        }
46
47        // Linking by email is safe only because the verifier asserted
48        // email_verified: Google is authoritative for the address.
49        $user = $this->users->ofEmail($identity->email);
50
51        if ($user === null) {
52            [$name, $surnames] = $identity->nameParts();
53
54            $user = User::registerWithoutPassword(
55                UserId::generate(),
56                $identity->email,
57                new Currency(self::DEFAULT_CURRENCY),
58                $name,
59                $surnames,
60            );
61
62            $this->users->save($user);
63        }
64
65        $this->identities->save(new UserIdentity(IdentityProvider::GOOGLE, $identity->subject, $user->id()));
66
67        return $user;
68    }
69}