Upgrading from 2.x to 3.0
3.0 adds the optional login-confirmation feature. Three contracts change along the way:
- logs are typed against the new
AuthenticationLogInterface, no longer against theAbstractAuthenticationLogmapped superclass — a log can now be implemented without extending it; UserIdentity(identifier + user class) replaces the plain identifier. Two accounts of different classes could share an identifier, so one account's log silenced the other's notification andcreateLog()could attach a log to the wrong account. The class is now part of the uniqueness key and is persisted;- the persisted log is handed to the
NEW_DEVICEevent and to the notification, so consumers no longer query it back.
1. Log entity: constructor and user_class column (required)
AbstractAuthenticationLog takes the UserIdentity first and persists its userClass.
Before:
public function __construct(User $user, UserInformation $userInformation)
{
$this->user = $user;
parent::__construct($userInformation);
}After:
use Spiriit\Bundle\AuthLogBundle\DTO\UserIdentity;
public function __construct(User $user, UserIdentity $userIdentity, UserInformation $userInformation)
{
$this->user = $user;
parent::__construct($userIdentity, $userInformation);
}Implementing AuthenticationLogInterface without the mapped superclass? Write getUserClass(): string yourself.
Database migration
doctrine:migrations:diff generates ADD user_class VARCHAR(255) NOT NULL, which fails on PostgreSQL when the table already holds rows. Backfill in three steps:
ALTER TABLE user_auth_log ADD user_class VARCHAR(255) DEFAULT '' NOT NULL;
UPDATE user_auth_log SET user_class = 'App\Entity\User';
ALTER TABLE user_auth_log ALTER user_class DROP DEFAULT;Inside a PHP migration, escape the backslashes: $this->addSql("UPDATE user_auth_log SET user_class = 'App\\\\Entity\\\\User'");. Use one UPDATE per user class if a single table stores several. The column stays NOT NULL — it belongs to the uniqueness key.
That key is now (user, user_class, ip_address), so declare the matching index on your entity; a mapped superclass cannot do it for you:
#[ORM\Entity(repositoryClass: UserAuthLogRepository::class)]
#[ORM\Index(columns: ['user_id', 'user_class', 'ip_address'])]
class UserAuthLog extends AbstractAuthenticationLog2. Repository and creator: UserIdentity (required)
save() and createLog() now type-hint AuthenticationLogInterface, and findExistingLog() and createLog() receive a UserIdentity. A parameter type cannot be narrowed in an implementation, so an outdated signature is a fatal error at class load.
Before:
use Spiriit\Bundle\AuthLogBundle\Entity\AbstractAuthenticationLog;
public function save(AbstractAuthenticationLog $log): void
public function findExistingLog(string $userIdentifier, UserInformation $userInformation): bool
{
return null !== $this->findOneBy([
'user' => $userIdentifier,
'ipAddress' => $userInformation->ipAddress,
]);
}
public function createLog(string $userIdentifier, UserInformation $userInformation): AbstractAuthenticationLog
{
$user = $this->getEntityManager()->getRepository(User::class)->findOneBy([
'email' => $userIdentifier,
]);
return new UserAuthLog($user, $userInformation);
}After:
use Spiriit\Bundle\AuthLogBundle\DTO\UserIdentity;
use Spiriit\Bundle\AuthLogBundle\Entity\AuthenticationLogInterface;
public function save(AuthenticationLogInterface $log): void
public function findExistingLog(UserIdentity $userIdentity, UserInformation $userInformation): bool
{
$user = $this->findUser($userIdentity);
if (null === $user) {
return false;
}
return null !== $this->findOneBy([
'user' => $user,
'userClass' => $userIdentity->userClass,
'ipAddress' => $userInformation->ipAddress,
]);
}
public function createLog(UserIdentity $userIdentity, UserInformation $userInformation): AuthenticationLogInterface
{
return new UserAuthLog($this->findUser($userIdentity), $userIdentity, $userInformation);
}
private function findUser(UserIdentity $userIdentity): ?User
{
return $this->getEntityManager()->getRepository(User::class)->findOneBy([
'email' => $userIdentity->userIdentifier,
]);
}Note: the 2.x example passed the identifier string as the
usercriterion, comparing a relation to an email. Load the user first, as above.
3. Custom notification: send() takes a NewDeviceNotification
Only if you implemented NotificationInterface for a custom transport. The argument list is replaced by a single object carrying the user reference, the user information, the persisted log and the confirmation links.
Before:
public function send(UserInformation $userInformation, UserReference $userReference, ?ConfirmationLinks $confirmationLinks = null): voidAfter:
use Spiriit\Bundle\AuthLogBundle\Notification\NewDeviceNotification;
public function send(NewDeviceNotification $notification): voidIts properties are listed on the custom notification page.
4. Custom handler: UserIdentity and returned log
Only if you replaced the default DoctrineAuthenticationLogHandler.
// Before
public function isKnown(string $userIdentifier, UserInformation $userInformation): bool
public function handle(string $userIdentifier, UserInformation $userInformation): void
// After
public function isKnown(UserIdentity $userIdentity, UserInformation $userInformation): bool
public function handle(UserIdentity $userIdentity, UserInformation $userInformation): AuthenticationLogInterfacehandle() returns the log it saved, so the caller can pass it to the event and the notification.
5. NEW_DEVICE listeners: nothing to change
userIdentifier() is still there. Two accessors are new:
$event->userIdentifier(); // unchanged
$event->userIdentity()->userClass; // new: the user FQCN
$event->authenticationLog(); // new: the persisted logOnly code that constructs the event — typically your tests — has to pass the three arguments.
6. Messenger: drain the queue before deploying
LoginParameterDto now carries a UserIdentity instead of a string, so the payload shape of AuthLoginMessage changes. A 2.x message decoded by 3.0 code is rejected (MessageDecodingFailedException) and those logins are lost. If you route it to an async transport, drain the queue first:
bin/console messenger:stop-workers # let the running workers finish
# consume until the transport is empty, then deploy and restart the workersThis also sidesteps the serializer transport needing symfony/property-info to denormalize the nested UserIdentity.
7. Twig template
The email context gains authenticationLog and userReference. authenticableLog still points to the same UserReference object but is deprecated, removed in 4.0: replace authenticableLog.userIdentifier with userReference.userIdentity.userIdentifier.
Reference
Changes not covered by the steps above, for custom code:
| Class | Change |
|---|---|
UserIdentity | New. userIdentifier + userClass; UserIdentity::fromUser() builds it from a UserInterface, resolving Doctrine proxies |
AuthenticationLogInterface | New. getUser(), getUserClass() and the read accessors. AbstractAuthenticationLog implements it |
LoginParameterDto, UserReference | userIdentifier (string) replaced by userIdentity |
MailerNotification | Template context gains authenticationLog and userReference |