Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ollieread/30b5f27b5e75c8a6b9042763642d9101 to your computer and use it in GitHub Desktop.

Select an option

Save ollieread/30b5f27b5e75c8a6b9042763642d9101 to your computer and use it in GitHub Desktop.
<?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);
}
}
}
<?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