makraz/verify-email-change-bundle

Symfony bundle for secure email address changes with verification

Installs: 7

Dependents: 0

Suggesters: 0

Security: 0

Stars: 4

Watchers: 0

Forks: 0

Open Issues: 0

Type:symfony-bundle

pkg:composer/makraz/verify-email-change-bundle

2.0.0 2026-02-09 15:00 UTC

This package is auto-updated.

Last update: 2026-02-10 21:13:33 UTC


README

License Tests

A Symfony bundle that provides secure email address change functionality with verification.

Features

  • Cryptographically Secure: Uses selector + hashed token pattern to prevent timing attacks
  • Configurable Expiration: Set custom lifetimes for verification links
  • Built-in Throttling: Prevents abuse with configurable rate limiting
  • Flexible: You control email sending, UI, and password verification
  • Twig Integration: Built-in Twig functions for checking pending email changes
  • Max Verification Attempts: Auto-invalidation after configurable failed attempts
  • Dual Verification Mode: Optional confirmation from both old and new email addresses
  • CSRF Protection: Built-in helper for cancel endpoint security
  • Email Notifications: Built-in EmailChangeNotifier service with Twig templates
  • Translations: Built-in translations for English, French, and Arabic
  • API/Headless Support: JSON response factory for SPA and mobile app integration
  • OTP Verification: Numeric code verification alternative to signed URLs
  • Audit Events: Security-relevant events for logging and compliance
  • Pluggable Persistence: Doctrine ORM, PSR-6 Cache, or in-memory adapters
  • Well Tested: Comprehensive test suite with 480+ tests
  • Event-Driven: Dispatches events for extensibility
  • Symfony Flex: Auto-discovery support for seamless installation

Installation

composer require makraz/verify-email-change-bundle

The bundle supports Symfony Flex auto-discovery and will be registered automatically. If you're not using Flex, enable it manually:

// config/bundles.php
return [
    // ...
    Makraz\Bundle\VerifyEmailChange\MakrazVerifyEmailChangeBundle::class => ['all' => true],
];

Quick Start

Step 1: Update Your User Entity

Your User entity must implement EmailChangeableInterface:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Makraz\Bundle\VerifyEmailChange\Model\EmailChangeableInterface;

#[ORM\Entity]
class User implements EmailChangeableInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180, unique: true)]
    private string $email;

    // ... rest of your entity

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): static
    {
        $this->email = $email;
        return $this;
    }
}

Note: EmailChangeableInterface only requires getId(), getEmail(), and setEmail().

Step 2: Create the Database Table

Run the following command to create the migration:

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate

Or create the table manually:

CREATE TABLE email_change_request (
    id INT AUTO_INCREMENT PRIMARY KEY,
    selector VARCHAR(20) UNIQUE NOT NULL,
    hashed_token VARCHAR(100) NOT NULL,
    requested_at DATETIME NOT NULL,
    expires_at DATETIME NOT NULL,
    new_email VARCHAR(180) NOT NULL,
    user_identifier VARCHAR(255) NOT NULL,
    INDEX email_change_selector_idx (selector),
    INDEX email_change_user_idx (user_identifier)
);

Step 3: Configure the Bundle (Optional)

# config/packages/verify_email_change.yaml
verify_email_change:
    lifetime: 3600              # Link expires after 1 hour (default)
    enable_throttling: true     # Prevent abuse (default: true)
    throttle_limit: 3600        # Wait time between requests (default: 1 hour)
    max_attempts: 5             # Max verification attempts before invalidation (default: 5)
    require_old_email_confirmation: false  # Require old email confirmation too (default: false)

Step 4: Create Your Controller

<?php

namespace App\Controller;

use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Attribute\Route;
use Makraz\Bundle\VerifyEmailChange\EmailChange\EmailChangeHelper;
use Makraz\Bundle\VerifyEmailChange\Exception\VerifyEmailChangeExceptionInterface;
use Doctrine\ORM\EntityManagerInterface;

class EmailChangeController extends AbstractController
{
    public function __construct(
        private readonly EmailChangeHelper $emailChangeHelper,
        private readonly EntityManagerInterface $entityManager,
        private readonly MailerInterface $mailer,
    ) {}

    #[Route('/account/email/change', name: 'app_email_change_request')]
    public function request(Request $request): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        // Your form handling here...
        $newEmail = $request->request->get('new_email');

        try {
            // Generate the verification signature
            $signature = $this->emailChangeHelper->generateSignature(
                'app_email_change_verify',
                $user,
                $newEmail
            );

            $this->entityManager->flush();

            // Send verification email to the NEW address
            $email = (new Email())
                ->to($newEmail)
                ->subject('Verify your new email address')
                ->html(sprintf(
                    'Click here to verify: <a href="%s">%s</a><br>This link will expire in 1 hour.',
                    $signature->getSignedUrl(),
                    $signature->getSignedUrl()
                ));

            $this->mailer->send($email);

            $this->addFlash('success', 'Verification email sent! Check your new inbox.');
        } catch (VerifyEmailChangeExceptionInterface $e) {
            $this->addFlash('error', $e->getReason());
        }

        return $this->redirectToRoute('app_profile');
    }

    #[Route('/account/email/verify', name: 'app_email_change_verify')]
    public function verify(Request $request): Response
    {
        try {
            $user = $this->emailChangeHelper->validateTokenAndFetchUser($request);
            $oldEmail = $this->emailChangeHelper->confirmEmailChange($user);
            $this->entityManager->flush();

            $this->addFlash('success', 'Email changed successfully!');
        } catch (VerifyEmailChangeExceptionInterface $e) {
            $this->addFlash('error', $e->getReason());
            return $this->redirectToRoute('app_email_change_request');
        }

        return $this->redirectToRoute('app_profile');
    }

    #[Route('/account/email/cancel', name: 'app_email_change_cancel', methods: ['POST'])]
    public function cancel(): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        $this->emailChangeHelper->cancelEmailChange($user);
        $this->entityManager->flush();

        $this->addFlash('success', 'Email change cancelled.');

        return $this->redirectToRoute('app_profile');
    }
}

Displaying Pending Email Changes

The bundle provides Twig functions to easily display pending email change status in your templates.

Using Twig Functions (Recommended)

{# templates/account/profile.html.twig #}

<h2>Email Address</h2>

<div>
    <strong>Current Email:</strong> {{ app.user.email }}
</div>

{% if has_pending_email_change(app.user) %}
    <div class="alert alert-info">
        <p>Pending email change to: <strong>{{ get_pending_email(app.user) }}</strong></p>
        <p>Please check your new email inbox for the verification link.</p>

        <form method="post" action="{{ path('app_email_change_cancel') }}">
            <button type="submit" class="btn btn-secondary">Cancel Email Change</button>
        </form>
    </div>
{% else %}
    <a href="{{ path('app_email_change_request') }}" class="btn btn-primary">
        Change Email Address
    </a>
{% endif %}

Available Twig Functions

  • has_pending_email_change(user): Returns true if the user has a pending, non-expired email change request
  • get_pending_email(user): Returns the pending new email address, or null if none exists

How It Works

Flow Diagram

User requests email change
    |
EmailChangeHelper::generateSignature()
    |
Token created & stored (hashed)
    |
Verification email sent to NEW address
    |
User clicks link
    |
EmailChangeHelper::validateTokenAndFetchUser()
    |
Token validated (timing-safe comparison)
    |
EmailChangeHelper::confirmEmailChange()
    |
Email updated, request deleted
    |
Notification sent to OLD address

API Reference

EmailChangeHelper

generateSignature()

public function generateSignature(
    string $routeName,
    EmailChangeableInterface $user,
    string $newEmail,
    array $extraParams = []
): EmailChangeSignature

validateTokenAndFetchUser()

public function validateTokenAndFetchUser(Request $request): EmailChangeableInterface

confirmEmailChange()

public function confirmEmailChange(EmailChangeableInterface $user): string

Returns the user's old email address.

cancelEmailChange()

public function cancelEmailChange(EmailChangeableInterface $user): void

hasPendingEmailChange()

public function hasPendingEmailChange(EmailChangeableInterface $user): bool

getPendingEmail()

public function getPendingEmail(EmailChangeableInterface $user): ?string

Maintenance

Purging Expired Requests

Expired email change requests remain in the database until purged. Use the built-in console command to clean them up:

# Purge all expired requests
php bin/console verify:email-change:purge-expired

# Preview what would be purged (no changes made)
php bin/console verify:email-change:purge-expired --dry-run

# Purge only requests that expired more than 24 hours ago
php bin/console verify:email-change:purge-expired --older-than=86400

# Combine options
php bin/console verify:email-change:purge-expired --dry-run --older-than=3600

Recommended: Add a cron job or Symfony Scheduler task to purge regularly:

# Run daily at midnight
0 0 * * * cd /path/to/project && php bin/console verify:email-change:purge-expired --older-than=86400

Or with Symfony Scheduler:

use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;

#[AsPeriodicTask('1 day')]
class PurgeExpiredEmailChangeRequestsMessage
{
    // This message triggers the purge command
}

Events

The bundle dispatches events for extensibility:

EmailChangeInitiatedEvent

Dispatched when an email change request is initiated.

use Makraz\Bundle\VerifyEmailChange\Event\EmailChangeInitiatedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
class EmailChangeListener
{
    public function __invoke(EmailChangeInitiatedEvent $event): void
    {
        $user = $event->getUser();
        $newEmail = $event->getNewEmail();
        $oldEmail = $event->getOldEmail();
        $verificationUrl = $event->getVerificationUrl();
    }
}

EmailChangeConfirmedEvent

Dispatched when an email change is confirmed.

EmailChangeCancelledEvent

Dispatched when an email change is cancelled.

Security

Dual Verification Mode

For high-security applications, enable dual verification to require confirmation from both the old and new email addresses:

# config/packages/verify_email_change.yaml
verify_email_change:
    require_old_email_confirmation: true

When enabled, generateSignature() returns an EmailChangeDualSignature with two URLs:

$signature = $this->emailChangeHelper->generateSignature(
    'app_email_change_verify',
    $user,
    $newEmail
);

// Send verification to the NEW email address
$this->sendEmail($newEmail, $signature->getSignedUrl());

// Also send confirmation to the OLD email address
if ($signature instanceof EmailChangeDualSignature) {
    $this->sendEmail($user->getEmail(), $signature->getOldEmailSignedUrl());
}

Handle old email verification in your controller:

#[Route('/account/email/verify-old', name: 'app_email_change_verify_old')]
public function verifyOldEmail(Request $request): Response
{
    try {
        $user = $this->emailChangeHelper->validateOldEmailToken($request);
        // Check if both confirmations are done
        $oldEmail = $this->emailChangeHelper->confirmEmailChange($user);
        $this->entityManager->flush();
        $this->addFlash('success', 'Email changed successfully!');
    } catch (VerifyEmailChangeExceptionInterface $e) {
        $this->addFlash('info', $e->getReason());
    }

    return $this->redirectToRoute('app_profile');
}

Max Verification Attempts

The bundle automatically invalidates verification links after a configurable number of failed attempts (default: 5):

verify_email_change:
    max_attempts: 5

After exceeding the limit, a TooManyVerificationAttemptsException is thrown and the request is removed.

CSRF Protection for Cancel Endpoint

The bundle provides a CsrfTokenHelper to protect the cancel endpoint:

use Makraz\Bundle\VerifyEmailChange\Security\CsrfTokenHelper;

class EmailChangeController extends AbstractController
{
    public function __construct(
        private readonly EmailChangeHelper $emailChangeHelper,
        private readonly CsrfTokenHelper $csrfHelper,
    ) {}

    #[Route('/account/email/cancel', name: 'app_email_change_cancel', methods: ['POST'])]
    public function cancel(Request $request): Response
    {
        if (!$this->csrfHelper->isTokenValid($request)) {
            throw $this->createAccessDeniedException('Invalid CSRF token.');
        }

        $this->emailChangeHelper->cancelEmailChange($this->getUser());
        $this->addFlash('success', 'Email change cancelled.');
        return $this->redirectToRoute('app_profile');
    }
}

In your Twig template:

<form method="post" action="{{ path('app_email_change_cancel') }}">
    <input type="hidden" name="_csrf_token" value="{{ csrf_token('email_change_cancel') }}">
    <button type="submit">Cancel Email Change</button>
</form>

Security Recommendations

  1. Always require password confirmation before initiating an email change
  2. Enable dual verification (require_old_email_confirmation: true) for sensitive applications
  3. Use CSRF protection on the cancel endpoint (see above)
  4. Send a notification to the old email address when an email change is initiated
  5. Keep verification link lifetimes short (1 hour is recommended)
  6. Set up the purge command as a cron job to clean expired requests
  7. Use HTTPS for all verification URLs (the bundle uses UrlGeneratorInterface::ABSOLUTE_URL)
  8. Monitor events — listen to EmailChangeInitiatedEvent for audit logging

Email Notifications

The bundle includes an optional EmailChangeNotifier service that handles sending verification and notification emails using the built-in Twig templates.

Enabling the Notifier

# config/packages/verify_email_change.yaml
verify_email_change:
    notifier:
        enabled: true
        sender_email: 'noreply@example.com'
        sender_name: 'My Application'  # optional

Using the Notifier

use Makraz\Bundle\VerifyEmailChange\EmailChange\EmailChangeHelper;
use Makraz\Bundle\VerifyEmailChange\Notifier\EmailChangeNotifier;

class EmailChangeController extends AbstractController
{
    public function __construct(
        private readonly EmailChangeHelper $emailChangeHelper,
        private readonly EmailChangeNotifier $notifier,
    ) {}

    public function request(Request $request): Response
    {
        $user = $this->getUser();
        $newEmail = $request->request->get('new_email');

        $signature = $this->emailChangeHelper->generateSignature(
            'app_email_change_verify',
            $user,
            $newEmail
        );

        // Sends verification to new email (and old email in dual mode)
        $this->notifier->sendVerificationEmail($user, $newEmail, $signature);

        return $this->redirectToRoute('app_profile');
    }

    public function verify(Request $request): Response
    {
        $user = $this->emailChangeHelper->validateTokenAndFetchUser($request);
        $oldEmail = $this->emailChangeHelper->confirmEmailChange($user);

        // Notify old email address about the change
        $this->notifier->sendEmailChangeConfirmation($user, $oldEmail, $user->getEmail());

        return $this->redirectToRoute('app_profile');
    }
}

Customizing Email Templates

Override the default templates by creating files in your project:

templates/bundles/MakrazVerifyEmailChange/email/
    verify_new_email.html.twig       # Verification email to new address
    confirm_old_email.html.twig      # Confirmation email to old address (dual mode)
    email_change_confirmed.html.twig # Change complete notification
    email_change_cancelled.html.twig # Cancellation notification

Translations

The bundle includes translations for exception messages and email templates in:

  • English (en)
  • French (fr)
  • Arabic (ar)

Translations are loaded automatically. To override them, create your own translation files:

# translations/verify_email_change.en.yaml
verify_email_change:
    exception:
        same_email: "Your custom message here"
    notification:
        verify_subject: "Custom subject"

API / Headless Mode

For SPA, mobile apps, and API-first applications, use the EmailChangeResponseFactory to generate consistent JSON responses:

use Makraz\Bundle\VerifyEmailChange\Api\EmailChangeResponseFactory;
use Makraz\Bundle\VerifyEmailChange\Exception\VerifyEmailChangeExceptionInterface;

class ApiEmailChangeController
{
    public function __construct(
        private readonly EmailChangeHelper $emailChangeHelper,
        private readonly EmailChangeResponseFactory $responseFactory,
    ) {}

    #[Route('/api/email/change', methods: ['POST'])]
    public function request(Request $request): JsonResponse
    {
        try {
            $user = $this->getUser();
            $newEmail = $request->toArray()['new_email'];

            $signature = $this->emailChangeHelper->generateSignature(
                'api_email_change_verify', $user, $newEmail
            );

            return $this->responseFactory->initiated($newEmail, $signature->getExpiresAt());
        } catch (VerifyEmailChangeExceptionInterface $e) {
            return $this->responseFactory->error($e);
        }
    }

    #[Route('/api/email/status', methods: ['GET'])]
    public function status(): JsonResponse
    {
        $user = $this->getUser();
        return $this->responseFactory->pendingStatus(
            $this->emailChangeHelper->getPendingEmail($user)
        );
    }
}

OTP Verification

For mobile apps and API flows, use numeric OTP codes instead of signed URL links:

# config/packages/verify_email_change.yaml
verify_email_change:
    otp:
        enabled: true
        length: 6  # 4-10 digits
use Makraz\Bundle\VerifyEmailChange\Otp\OtpEmailChangeHelper;

class OtpEmailChangeController
{
    public function __construct(
        private readonly OtpEmailChangeHelper $otpHelper,
    ) {}

    #[Route('/api/email/otp/request', methods: ['POST'])]
    public function request(Request $request): JsonResponse
    {
        $user = $this->getUser();
        $newEmail = $request->toArray()['new_email'];

        $result = $this->otpHelper->generateOtp($user, $newEmail);

        // Send the OTP to the new email address
        // $this->mailer->send(...$result->getOtp()...)

        return new JsonResponse([
            'message' => 'OTP sent to new email address.',
            'expires_at' => $result->getExpiresAt()->format(\DateTimeInterface::ATOM),
        ]);
    }

    #[Route('/api/email/otp/verify', methods: ['POST'])]
    public function verify(Request $request): JsonResponse
    {
        $user = $this->getUser();
        $otp = $request->toArray()['otp'];

        $oldEmail = $this->otpHelper->verifyOtp($user, $otp);

        return new JsonResponse([
            'message' => 'Email changed successfully.',
            'old_email' => $oldEmail,
            'new_email' => $user->getEmail(),
        ]);
    }
}

Audit Events

The EmailChangeAuditEvent provides security-relevant information for logging:

use Makraz\Bundle\VerifyEmailChange\Event\EmailChangeAuditEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
class EmailChangeAuditListener
{
    public function __invoke(EmailChangeAuditEvent $event): void
    {
        $action = $event->getAction(); // 'initiated', 'confirmed', 'failed_verification', etc.
        $user = $event->getUser();
        $ip = $event->getIpAddress();
        $metadata = $event->getMetadata();

        // Log to your audit system
    }
}

Available actions: initiated, verified, confirmed, cancelled, failed_verification, max_attempts_exceeded, expired_access, old_email_confirmed.

Storage Backends

The bundle supports multiple storage backends. The default is database (Doctrine ORM).

Database (default)

verify_email_change:
    storage: database

Uses Doctrine ORM to persist email change requests. Requires doctrine/orm and doctrine/doctrine-bundle.

Stateless

verify_email_change:
    storage: stateless

Uses a PSR-6 cache pool (Redis, Memcached, filesystem, etc.) instead of a database. Ideal for applications that don't use Doctrine ORM or want faster lookups. Requires a CacheItemPoolInterface service and a user provider callback.

In-Memory (Testing)

The InMemoryEmailChangeRequestRepository is intended for testing. Register it as a service manually:

# config/packages/test/verify_email_change.yaml
verify_email_change:
    storage_service: 'app.in_memory_email_change_repository'

Custom Adapter

Implement EmailChangeRequestRepositoryInterface and point the configuration to your service:

verify_email_change:
    storage_service: 'App\Repository\MyEmailChangeRequestRepository'

When storage_service is set, it takes precedence over the storage option.

Configuration Reference

verify_email_change:
    # Time in seconds that an email change request is valid
    # Min: 60 (1 minute), Max: 86400 (24 hours)
    lifetime: 3600  # default: 1 hour

    # Enable request throttling to prevent abuse
    enable_throttling: true  # default: true

    # Time in seconds before a new request can be made
    # Only used if enable_throttling is true
    throttle_limit: 3600  # default: 1 hour

    # Maximum number of failed verification attempts before invalidation
    max_attempts: 5  # default: 5

    # Require confirmation from both old and new email addresses
    require_old_email_confirmation: false  # default: false

    # Storage backend: "database" (Doctrine ORM) or "stateless" (PSR-6 cache)
    storage: database  # default: database

    # Custom service ID for the repository (overrides storage option)
    storage_service: ~  # default: null

    # OTP verification mode (alternative to signed URLs)
    otp:
        enabled: false  # default: false
        length: 6       # default: 6 (4-10 digits)

    # Optional email notifier service
    notifier:
        enabled: false  # default: false
        sender_email: ~  # required when enabled
        sender_name: ~   # optional

Exception Reference

All exceptions implement VerifyEmailChangeExceptionInterface.

Exception When Message
SameEmailException New email equals current email "The new email address is identical to the current one."
EmailAlreadyInUseException Email taken by another user "This email address is already in use."
TooManyEmailChangeRequestsException Request too soon after previous "You have already requested an email change..."
TooManyVerificationAttemptsException Max attempts exceeded "Too many verification attempts (max: N)."
ExpiredEmailChangeRequestException Verification link expired "The email change link has expired."
InvalidEmailChangeRequestException Invalid or tampered link Varies
use Makraz\Bundle\VerifyEmailChange\Exception\VerifyEmailChangeExceptionInterface;

try {
    // Your email change logic
} catch (VerifyEmailChangeExceptionInterface $e) {
    $this->addFlash('error', $e->getReason());
}

Upgrading

From v1.4 to v2.0

Breaking changes:

  • EmailChangeInterface has been removed. Use EmailChangeableInterface instead.
  • Persistence\EmailChangeRequestRepository has been removed. Use Persistence\Doctrine\DoctrineEmailChangeRequestRepository instead.
  • persistence config option renamed to storage (values: database, stateless).
  • persistence_service config option renamed to storage_service.

New features:

  • EmailChangeResponseFactory for JSON/API responses
  • OTP-based email verification (OtpEmailChangeHelper)
  • EmailChangeAuditEvent for security logging

Migration:

-use Makraz\Bundle\VerifyEmailChange\Model\EmailChangeInterface;
+use Makraz\Bundle\VerifyEmailChange\Model\EmailChangeableInterface;

-class User implements EmailChangeInterface
+class User implements EmailChangeableInterface
 # config/packages/verify_email_change.yaml
 verify_email_change:
-    persistence: doctrine
+    storage: database

-    persistence_service: 'App\Repository\MyRepo'
+    storage_service: 'App\Repository\MyRepo'

From v1.3 to v1.4

New features (non-breaking):

  • Pluggable persistence adapters: Doctrine ORM, PSR-6 Cache, In-Memory
  • DoctrineEmailChangeRequestRepository moved to Persistence\Doctrine namespace
  • storage and storage_service configuration options
  • EmailChangeRequestRepository is now deprecated (use DoctrineEmailChangeRequestRepository)

No database migration required.

From v1.2 to v1.3

New features (non-breaking):

  • Translation support for English, French, and Arabic
  • Default Twig email templates (@MakrazVerifyEmailChange/email/...)
  • Optional EmailChangeNotifier service for sending emails

No database migration required. Enable the notifier in configuration if desired.

From v1.1 to v1.2

New features (non-breaking):

  • Max verification attempts protection — enabled by default (5 attempts). Configure with max_attempts.
  • Dual verification mode — opt-in with require_old_email_confirmation: true.
  • CSRF token helper — optional service for cancel endpoint protection.

Database migration required: The email_change_request table has new columns:

ALTER TABLE email_change_request
    ADD attempts INT DEFAULT 0 NOT NULL,
    ADD confirmed_by_new_email TINYINT(1) DEFAULT 0 NOT NULL,
    ADD confirmed_by_old_email TINYINT(1) DEFAULT 0 NOT NULL,
    ADD old_email_hashed_token VARCHAR(100) DEFAULT NULL,
    ADD old_email_selector VARCHAR(20) DEFAULT NULL;
CREATE UNIQUE INDEX email_change_old_selector_idx ON email_change_request (old_email_selector);

Or use Doctrine migrations:

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate

From v1.0 to v1.1

Interface change (non-breaking):

  • EmailChangeInterface is now deprecated. Migrate to EmailChangeableInterface which only requires getId(), getEmail(), and setEmail().
  • Classes implementing EmailChangeInterface continue to work without changes.
-use Makraz\Bundle\VerifyEmailChange\Model\EmailChangeInterface;
+use Makraz\Bundle\VerifyEmailChange\Model\EmailChangeableInterface;

-class User implements EmailChangeInterface
+class User implements EmailChangeableInterface
 {
-    // Can remove hasPendingEmailChange() and getPendingEmail()
 }

Testing

composer install
vendor/bin/phpunit

License

This bundle is released under the MIT License. See the LICENSE file for details.

Support

For issues, questions, or contributions, please visit: https://github.com/makraz/verify-email-change-bundle