Created
March 11, 2026 11:35
-
-
Save ollieread/30b5f27b5e75c8a6b9042763642d9101 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| namespace Icarus\Kernel\Persistence; | |
| use Carbon\CarbonImmutable; | |
| use Icarus\Domain\Shared\Id; | |
| use Icarus\Domain\Shared\RecordsEvents; | |
| use Icarus\Kernel\Concerns\HandlesIlluminateConnections; | |
| use Icarus\Kernel\Contracts\EventDispatcher; | |
| use Icarus\Kernel\Contracts\RecordsLifecycleEvents; | |
| use Icarus\Kernel\IdentityMap; | |
| use Icarus\Kernel\SnapshotMap; | |
| use Illuminate\Database\ConnectionInterface; | |
| use stdClass; | |
| /** | |
| * @template TAggregateRoot of object | |
| */ | |
| abstract class IlluminateBaseRepository | |
| { | |
| use HandlesIlluminateConnections; | |
| /** | |
| * @var class-string<TAggregateRoot> | |
| */ | |
| private string $aggregateClass; | |
| /** | |
| * @var \Icarus\Kernel\IdentityMap | |
| */ | |
| private(set) protected IdentityMap $identityMap; | |
| /** | |
| * @var \Icarus\Kernel\SnapshotMap | |
| */ | |
| private(set) protected SnapshotMap $snapshotMap; | |
| /** | |
| * @var \Icarus\Kernel\Contracts\EventDispatcher | |
| */ | |
| private(set) protected EventDispatcher $dispatcher; | |
| /** | |
| * @param class-string<TAggregateRoot> $aggregateClass | |
| * @param \Illuminate\Database\ConnectionInterface $connection | |
| * @param \Icarus\Kernel\IdentityMap $identityMap | |
| * @param \Icarus\Kernel\SnapshotMap $snapshotMap | |
| * @param \Icarus\Kernel\Contracts\EventDispatcher $dispatcher | |
| */ | |
| public function __construct( | |
| string $aggregateClass, | |
| ConnectionInterface $connection, | |
| IdentityMap $identityMap, | |
| SnapshotMap $snapshotMap, | |
| EventDispatcher $dispatcher | |
| ) | |
| { | |
| $this->aggregateClass = $aggregateClass; | |
| $this->identityMap = $identityMap; | |
| $this->snapshotMap = $snapshotMap; | |
| $this->dispatcher = $dispatcher; | |
| $this->setConnection($connection); | |
| } | |
| /** | |
| * @param array<string, mixed>|\stdClass $results | |
| * | |
| * @return object | |
| * | |
| * @phpstan-return TAggregateRoot | |
| */ | |
| abstract protected function hydrate(array|stdClass $results): object; | |
| /** | |
| * @param object $aggregate | |
| * | |
| * @phpstan-param TAggregateRoot $aggregate | |
| * | |
| * @return array<string, mixed> | |
| */ | |
| abstract protected function dehydrate(object $aggregate): array; | |
| protected function shouldCreate(Id $id): bool | |
| { | |
| return $this->snapshotMap->has($id, $this->aggregateClass) === false; | |
| } | |
| /** | |
| * @param \Icarus\Domain\Shared\Id $id | |
| * @param object $aggregate | |
| * @param string $table | |
| * | |
| * @phpstan-param TAggregateRoot $aggregate | |
| * | |
| * @return bool | |
| */ | |
| protected function create(Id $id, object $aggregate, string $table): bool | |
| { | |
| $fields = $this->dehydrate($aggregate); | |
| $toInsert = $fields; | |
| if ($this instanceof RecordsLifecycleEvents) { | |
| $toInsert['created_at'] = $toInsert['updated_at'] = CarbonImmutable::now(); | |
| } | |
| $success = $this->query() | |
| ->from($table) | |
| ->insert($toInsert); | |
| // If it was successful, we have things to do. | |
| if ($success) { | |
| $this->handlePostPersist($id, $aggregate, $fields, true); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * @param \Icarus\Domain\Shared\Id $id | |
| * @param object $aggregate | |
| * @param string $table | |
| * @param string $key | |
| * | |
| * @phpstan-param TAggregateRoot $aggregate | |
| * | |
| * @return bool | |
| */ | |
| protected function update(Id $id, object $aggregate, string $table, string $key = 'id'): bool | |
| { | |
| $fields = $this->dehydrate($aggregate); | |
| $toUpdate = $this->snapshotMap->toPersist($id, $this->aggregateClass, $fields); | |
| if (empty($toUpdate)) { | |
| return true; | |
| } | |
| if ($this instanceof RecordsLifecycleEvents) { | |
| $toUpdate['updated_at'] = CarbonImmutable::now(); | |
| } | |
| $success = $this->query() | |
| ->from($table) | |
| ->where($key, '=', $id) | |
| ->update($toUpdate); | |
| // If it was successful, we have things to do. | |
| if ($success) { | |
| $this->handlePostPersist($id, $aggregate, $fields, false); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * @param \Icarus\Domain\Shared\Id $id | |
| * @param object $aggregate | |
| * @param bool $force | |
| * | |
| * @phpstan-param TAggregateRoot $aggregate | |
| * | |
| * @return void | |
| */ | |
| protected function storeIdentity(Id $id, object $aggregate, bool $force = true): void | |
| { | |
| if ($force || ! $this->identityMap->has($id, $this->aggregateClass)) { | |
| $this->identityMap->put($id, $aggregate); | |
| } | |
| } | |
| /** | |
| * @param \Icarus\Domain\Shared\Id $id | |
| * @param array<string, mixed> $fields | |
| * | |
| * @return void | |
| */ | |
| protected function storeSnapshot(Id $id, array $fields): void | |
| { | |
| $this->snapshotMap->put($id, $this->aggregateClass, $fields); | |
| } | |
| /** | |
| * @param \Icarus\Domain\Shared\Id $id | |
| * @param object $aggregate | |
| * @param array<string, mixed> $fields | |
| * | |
| * @phpstan-param TAggregateRoot $aggregate | |
| * | |
| * @return void | |
| */ | |
| protected function handlePostPersist(Id $id, object $aggregate, array $fields, bool $storeIdentity): void | |
| { | |
| if ($storeIdentity) { | |
| $this->storeIdentity($id, $aggregate); | |
| } | |
| $this->storeSnapshot($id, $fields); | |
| // And dispatch any necessary events. | |
| if ($aggregate instanceof RecordsEvents) { | |
| $this->dispatcher->dispatchFrom($aggregate); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| namespace Icarus\Kernel\User; | |
| use Icarus\Domain\User\User; | |
| use Icarus\Domain\User\UserHydrator; | |
| use Icarus\Domain\User\UserId; | |
| use Icarus\Domain\User\UserRepository; | |
| use Icarus\Kernel\Contracts\EventDispatcher; | |
| use Icarus\Kernel\Contracts\RecordsLifecycleEvents; | |
| use Icarus\Kernel\IdentityMap; | |
| use Icarus\Kernel\Persistence\IlluminateBaseRepository; | |
| use Icarus\Kernel\SnapshotMap; | |
| use Illuminate\Database\ConnectionInterface; | |
| use stdClass; | |
| /** | |
| * @phpstan-import-type UserData from \Icarus\Domain\User\UserHydrator | |
| * | |
| * @extends \Icarus\Kernel\Persistence\IlluminateBaseRepository<\Icarus\Domain\User\User> | |
| */ | |
| final class IlluminateUserRepository extends IlluminateBaseRepository implements UserRepository, RecordsLifecycleEvents | |
| { | |
| public const string TABLE = 'users'; | |
| public const array FIELDS = [ | |
| 'id', | |
| 'name', | |
| 'email', | |
| 'password', | |
| 'active', | |
| 'operates_in', | |
| 'verified_at' | |
| ]; | |
| /** | |
| * @var \Icarus\Domain\User\UserHydrator | |
| */ | |
| private UserHydrator $hydrator; | |
| public function __construct( | |
| UserHydrator $hydrator, | |
| ConnectionInterface $connection, | |
| IdentityMap $identityMap, | |
| SnapshotMap $snapshotMap, | |
| EventDispatcher $dispatcher | |
| ) | |
| { | |
| $this->hydrator = $hydrator; | |
| parent::__construct( | |
| User::class, | |
| $connection, | |
| $identityMap, | |
| $snapshotMap, | |
| $dispatcher | |
| ); | |
| } | |
| /** | |
| * @param array<string, mixed>|\stdClass $results | |
| * | |
| * @phpstan-param UserData|stdClass $results | |
| * | |
| * @return \Icarus\Domain\User\User | |
| */ | |
| protected function hydrate(array|stdClass $results): User | |
| { | |
| $results = (array)$results; | |
| /** | |
| * @var UserData $results | |
| */ | |
| // Hydrate a fresh user object. | |
| $user = $this->hydrator->hydrate($results); | |
| // Make sure a snapshot is stored too. | |
| $this->storeSnapshot($user->id, $results); | |
| // And store the user in the identity map. | |
| $this->storeIdentity($user->id, $user); | |
| return $user; | |
| } | |
| /** | |
| * @param object $aggregate | |
| * | |
| * @phpstan-param User $aggregate | |
| * | |
| * @return array<string, mixed> | |
| * | |
| * @throws \JsonException | |
| */ | |
| protected function dehydrate(object $aggregate): array | |
| { | |
| return $this->hydrator->dehydrate($aggregate); | |
| } | |
| /** | |
| * Save the user. | |
| * | |
| * Returns <code>true</code> if the user was saved successfully, | |
| * <code>false</code> otherwise. | |
| * | |
| * @param \Icarus\Domain\User\User $user | |
| * | |
| * @return bool | |
| */ | |
| public function save(User $user): bool | |
| { | |
| return $this->shouldCreate($user->id) | |
| ? $this->create($user->id, $user, self::TABLE) | |
| : $this->update($user->id, $user, self::TABLE); | |
| } | |
| /** | |
| * Find a user by its ID. | |
| * | |
| * @param \Icarus\Domain\User\UserId $id | |
| * | |
| * @return \Icarus\Domain\User\User|null | |
| */ | |
| public function find(UserId $id): ?User | |
| { | |
| // Short-circuit and use the object from the identity map if it exists. | |
| if ($this->identityMap->has($id, User::class)) { | |
| return $this->identityMap->get($id, User::class); | |
| } | |
| $results = $this->query() | |
| ->select(self::FIELDS) | |
| ->where('id', $id) | |
| ->from(self::TABLE) | |
| ->first(); | |
| if ($results === null) { | |
| return null; | |
| } | |
| return $this->hydrate($results); | |
| } | |
| /** | |
| * Find a user by its email address. | |
| * | |
| * @param string $email | |
| * | |
| * @return \Icarus\Domain\User\User|null | |
| */ | |
| public function findByEmail(string $email): ?User | |
| { | |
| $results = $this->query() | |
| ->select(self::FIELDS) | |
| ->where('email', '=', $email) | |
| ->from(self::TABLE) | |
| ->first(); | |
| if ($results === null) { | |
| return null; | |
| } | |
| $results = (array)$results; | |
| /** @var UserData $results */ | |
| $id = new UserId($results['id']); | |
| // Return an existing user from the identity map if it exists, | |
| // otherwise hydrate a new user object. | |
| return $this->identityMap->get($id, User::class) | |
| ?? $this->hydrate($results); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment