Skip to content

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 the AbstractAuthenticationLog mapped 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 and createLog() 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_DEVICE event 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:

php
public function __construct(User $user, UserInformation $userInformation)
{
    $this->user = $user;
    parent::__construct($userInformation);
}

After:

php
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:

sql
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:

php
#[ORM\Entity(repositoryClass: UserAuthLogRepository::class)]
#[ORM\Index(columns: ['user_id', 'user_class', 'ip_address'])]
class UserAuthLog extends AbstractAuthenticationLog

2. 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:

php
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:

php
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 user criterion, 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:

php
public function send(UserInformation $userInformation, UserReference $userReference, ?ConfirmationLinks $confirmationLinks = null): void

After:

php
use Spiriit\Bundle\AuthLogBundle\Notification\NewDeviceNotification;

public function send(NewDeviceNotification $notification): void

Its properties are listed on the custom notification page.

4. Custom handler: UserIdentity and returned log

Only if you replaced the default DoctrineAuthenticationLogHandler.

php
// 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): AuthenticationLogInterface

handle() 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:

php
$event->userIdentifier();            // unchanged
$event->userIdentity()->userClass;   // new: the user FQCN
$event->authenticationLog();         // new: the persisted log

Only 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:

bash
bin/console messenger:stop-workers   # let the running workers finish
# consume until the transport is empty, then deploy and restart the workers

This 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:

ClassChange
UserIdentityNew. userIdentifier + userClass; UserIdentity::fromUser() builds it from a UserInterface, resolving Doctrine proxies
AuthenticationLogInterfaceNew. getUser(), getUserClass() and the read accessors. AbstractAuthenticationLog implements it
LoginParameterDto, UserReferenceuserIdentifier (string) replaced by userIdentity
MailerNotificationTemplate context gains authenticationLog and userReference

Built and maintained by Spiriit — released under the MIT License.