Repository
Your repository implements two interfaces:
AuthenticationLogRepositoryInterface— tell whether a log already exists, and save new onesAuthenticationLogCreatorInterface— build the log entity
Both receive a UserIdentity, a final readonly DTO carrying userIdentifier (what UserInterface::getUserIdentifier() returns) and userClass (the FQCN, resolved through Doctrine proxies). Keep the class in the lookup: two accounts of different classes may share the same identifier.
use Doctrine\ORM\EntityRepository;
use Spiriit\Bundle\AuthLogBundle\AuthenticationLog\AuthenticationLogCreatorInterface;
use Spiriit\Bundle\AuthLogBundle\DTO\UserIdentity;
use Spiriit\Bundle\AuthLogBundle\Entity\AuthenticationLogInterface;
use Spiriit\Bundle\AuthLogBundle\FetchUserInformation\UserInformation;
use Spiriit\Bundle\AuthLogBundle\Repository\AuthenticationLogRepositoryInterface;
class UserAuthLogRepository extends EntityRepository implements
AuthenticationLogRepositoryInterface,
AuthenticationLogCreatorInterface
{
public function save(AuthenticationLogInterface $log): void
{
$this->getEntityManager()->persist($log);
$this->getEntityManager()->flush();
}
public function findExistingLog(UserIdentity $userIdentity, UserInformation $userInformation): bool
{
return null !== $this->findOneBy([
'userIdentifier' => $userIdentity->userIdentifier,
'userClass' => $userIdentity->userClass,
'ipAddress' => $userInformation->ipAddress,
]);
}
public function createLog(UserIdentity $userIdentity, UserInformation $userInformation): AuthenticationLogInterface
{
$user = $this->findUser($userIdentity);
if (null === $user) {
throw new \RuntimeException(sprintf('No user found for identifier "%s".', $userIdentity->userIdentifier));
}
return new UserAuthLog($user, $userIdentity, $userInformation);
}
private function findUser(UserIdentity $userIdentity): ?User
{
return $this->getEntityManager()->getRepository(User::class)->findOneBy([
'email' => $userIdentity->userIdentifier,
]);
}
}That's it!
The bundle listens to LoginSuccessEvent, checks whether the context is known, persists the log and sends the notification on its own.
Defining a "known context"
findExistingLog() is what defines a known context for your application. The example above matches the identity and the IP address — widen it to the user agent or the location for a stricter definition.
The lookup reads the log's own user_identifier and user_class columns, so it costs a single query: only createLog() needs the User entity itself.
With login confirmation enabled, also exclude the logs the user disavowed and those revoked by the disavowal reactions — otherwise a reported context would still count as known and silence future alerts:
use Spiriit\Bundle\AuthLogBundle\Entity\AuthenticationLogStatus;
return null !== $this->findOneBy([
'userIdentifier' => $userIdentity->userIdentifier,
'userClass' => $userIdentity->userClass,
'ipAddress' => $userInformation->ipAddress,
'status' => [AuthenticationLogStatus::PENDING, AuthenticationLogStatus::ACKNOWLEDGED],
]);