Customer Registration Flow
Entry Point
Controller: Magento\Customer\Controller\Account\CreatePost::execute()
Route: POST /customer/account/createPost
Area: frontend
Execution Sequence
HTTP POST Request
User submits registration form with customer details (firstname, lastname, email, password, addresses)
Controller Pre-checks
- → Check if already logged in (exit if true)
- → Verify registration is allowed in configuration
- → Validate form_key (CSRF protection)
- → Regenerate session ID for security
Extract Customer Data
Convert POST data to CustomerInterface DTO
- → CustomerExtractor::extract('customer_account_create')
- → Build CustomerInterface DTO from POST data
- → Extract address if 'create_address' checkbox checked
- → Set newsletter subscription flag as extension attribute
Create Account
Call service contract: AccountManagementInterface::createAccount($customer, $password, $redirectUrl)
AccountManagement Internal Logic
- → Validate password strength against security policy
- → Check if email already exists (isEmailAvailable)
- → Hash password using Argon2ID13 algorithm
- → Set customer group (default or auto-assigned)
- → Set website_id from current store
- → Generate confirmation token if required
Repository Save
Persist customer to database via CustomerRepositoryInterface::save($customer, $passwordHash)
Transaction Wrapper Plugin (BEFORE)
- → Plugin: TransactionWrapper::beforeSave() [sortOrder: -1]
- → Opens database transaction
- → Ensures atomicity of customer + address save
Database Persistence
- → Validate customer data (email format, required fields)
- → Check unique email constraint (email + website_id)
- → Convert DTO to Model (Customer\Model\Customer)
- → INSERT into customer_entity (main table)
- → INSERT into customer_entity_* (EAV attribute values)
- → INSERT into customer_address_entity (if address provided)
Event Dispatch: customer_save_after_data_object
Event data: customerDataObject, origCustomerDataObject
Transaction Commit
- → Plugin: TransactionWrapper::afterSave()
- → Commits database transaction
- → Rolls back on any exception
Post-Save Actions
- → Process newsletter subscription (if opted in)
- → Send welcome email (or confirmation email if required)
- → Return saved CustomerInterface DTO
Registration Success Event
Dispatch event: customer_register_success
- → Check confirmation status
- → If confirmed: Log customer in automatically
- → If pending: Show "check email" message
Session Creation (if auto-confirmed)
- → Session::setCustomerDataAsLoggedIn($customer)
- → Dispatch event: customer_login
- → Dispatch event: customer_data_object_login
- → Delete mage-cache-sessid cookie (FPC invalidation)
HTTP Response
Redirect to customer dashboard or requested URL
Key Points
Transaction Safety
The TransactionWrapper plugin (sortOrder -1) runs BEFORE all other plugins and ensures customer + addresses are saved atomically. Rolls back on any validation or database error.
Email Confirmation
If customer/create_account/confirm is enabled, confirmation is required. Customer receives email with confirmation link. Account is inactive until confirmed.
Newsletter Subscription
Stored as extension attribute during registration. Processed by AccountManagement after customer save. Separate subscription record created in newsletter tables.
Customer Login Flow
Entry Point
Controller: Magento\Customer\Controller\Account\LoginPost::execute()
Route: POST /customer/account/loginPost
Area: frontend
Execution Sequence
HTTP POST Request
User submits login form credentials (login[username], login[password])
Controller Pre-checks
- → Validate form_key (CSRF protection)
- → Check if already logged in (exit if true)
- → Validate POST method
Authenticate Customer
Call service contract: AccountManagementInterface::authenticate($email, $password)
Authentication Logic
- → Load customer by email + website_id
- → Check if customer account is active
- → Check if email confirmation is pending
- → Delegate to AuthenticationInterface for password verification
Password Validation
- → Load customer model by ID
- → Retrieve password_hash from customer_entity
- → Verify password using Encryptor::validateHash()
- → Check if password hash algorithm is outdated
- → Process lock mechanism (failed login attempts)
- → Throw exception if authentication fails
Authentication Event
Dispatch event: customer_customer_authenticated
Observer: CustomerGroupAuthenticate validates customer group is active
Password Hash Upgrade (if needed)
- → If hash uses old algorithm (SHA256, MD5)
- → Rehash with current algorithm (Argon2ID13)
- → UPDATE customer_entity.password_hash
- → Handled by UpgradeCustomerPasswordObserver
Create Customer Session
- → Session::setCustomerDataAsLoggedIn($customer)
- → Store customer data in session storage
- → Set customer_id, customer_group_id in session
- → Regenerate session ID (prevent session fixation)
- → Set session cookie
Login Events
Dispatch events: customer_login, customer_data_object_login
- → Observer: LogLastLoginAtObserver - UPDATE customer_log SET last_login_at = NOW()
- → Observer: Visitor\BindCustomerLoginObserver - UPDATE customer_visitor SET customer_id = ?
Post-Login Actions
- → Set success message
- → Clear cart persistent data if needed
- → Determine redirect URL (dashboard or referer)
- → Delete mage-cache-sessid cookie (FPC invalidation)
HTTP Response
Redirect to dashboard or requested page
Key Points
Session Security
Session ID regenerated on login to prevent session fixation attacks. Session validated on each request. Customer group stored in session for performance.
Failed Login Attempts
Tracked in customer_entity.failures_num, first_failure, lock_expires. After X failures (configurable), account temporarily locked. Lock duration configurable in admin.
Database Updates
customer_log.last_login_at timestamp updated. customer_visitor.customer_id links visitor session to customer. customer_entity.password_hash upgraded if using old algorithm.
Customer Save Flow
Entry Point
Service Contract: CustomerRepositoryInterface::save(CustomerInterface $customer, $passwordHash = null)
Implementation: Model\ResourceModel\CustomerRepository
Note: Core flow for any customer update operation
Execution Sequence
Repository Save Called
Origin: Controller, API, Command, Observer, etc.
Transaction Wrapper (BEFORE)
- → Plugin: TransactionWrapper::beforeSave() [sortOrder: -1]
- → Begin database transaction
- → Store original customer data for rollback if needed
Load Original Customer
- → If $customer->getId() exists: Load existing customer
- → Store original for comparison in events
- → If new customer: Original is null
Validation
- → Validate email format
- → Validate required fields (firstname, lastname, etc.)
- → Check email uniqueness (email + website_id)
- → Validate custom attribute values
- → Throw InputException if validation fails
DTO to Model Conversion
- → Convert CustomerInterface DTO to Customer Model
- → Set password_hash if provided
- → Set custom EAV attributes
- → Prepare for database persistence
Database Persistence
- → ResourceModel\Customer::save($customerModel)
- → UPDATE customer_entity (if existing) OR INSERT (if new)
- → UPDATE/INSERT customer_entity_varchar (EAV attributes)
- → UPDATE/INSERT customer_entity_int, _datetime, etc.
- → Update updated_at timestamp
Convert Back to DTO
- → Load fresh data from database (to get auto-IDs, etc.)
- → Convert Customer Model to CustomerInterface DTO
- → Prepare for return and event dispatch
Critical Event: customer_save_after_data_object
Event data: customerDataObject (new state), origCustomerDataObject (old state)
- → Observer: UpgradeOrderCustomerEmailObserver - If email changed: UPDATE sales_order SET customer_email = new email
- → Observer: UpgradeQuoteCustomerEmailObserver - If email changed: UPDATE quote SET customer_email = new email
Transaction Commit
- → Plugin: TransactionWrapper::afterSave()
- → Commit database transaction
- → All changes persisted atomically
- → On exception: Rollback transaction and re-throw
Return Updated DTO
Returns CustomerInterface DTO to caller with updated data including auto-generated IDs
Key Points
Transaction Wrapper Pattern
Critical plugin with sortOrder -1 runs first. Wraps entire save operation in database transaction. Ensures customer + addresses + EAV attributes saved atomically. Rolls back on ANY exception.
Email Synchronization Side Effect
When customer email changes, observers update related records. All historical orders get new email for customer lookup. Active quote gets new email for cart recovery emails. This is a side effect of customer save, not explicit business logic.
Database Tables Modified
customer_entity (main record), customer_entity_varchar/int/etc (EAV attributes), sales_order (email sync via observer), quote (email sync via observer)
Customer Email Change Flow
Entry Point
Trigger: Any operation that calls CustomerRepositoryInterface::save() with a changed email
Note: Critical business flow - email is primary customer identifier
Execution Sequence
Customer Email Updated
User updates email in profile or admin changes it via CustomerRepository::save()
Repository Save Process
See "Customer Save Flow" above for full details. Database updated with new email.
Email Change Event
Event: customer_save_after_data_object
customerDataObject.email = "new@example.com"
origCustomerDataObject.email = "old@example.com"
Order Email Synchronization
Observer: UpgradeOrderCustomerEmailObserver
- → Check if origCustomerDataObject exists (not new customer)
- → Compare old email vs. new email
- → If changed: Build SearchCriteria (customer_id + old email)
- → OrderRepository::getList($searchCriteria)
- → Iterate all matching orders
- → Set new email: $order->setCustomerEmail($newEmail)
- → Collection save: $orders->save()
- → Side Effect: Historical orders now searchable by new email
Quote Email Synchronization
Observer: UpgradeQuoteCustomerEmailObserver
- → Check if origCustomerDataObject exists (not new customer)
- → Compare old email vs. new email
- → If changed: Try to load active quote
- → QuoteRepository::getForCustomer($customerId)
- → If quote exists: $quote->setCustomerEmail($newEmail)
- → QuoteRepository::save($quote)
- → Catch NoSuchEntityException (no active cart)
- → Side Effect: Cart recovery emails sent to new address
Database State After Email Change
- → customer_entity.email = "new@example.com"
- → sales_order.customer_email = "new@example.com" (all orders)
- → quote.customer_email = "new@example.com" (active cart)
Key Points
Why Synchronize Email?
Order Lookup: Admin "View Orders" searches by customer_email. Customer Reports: Order reports grouped by customer_email. Cart Recovery: Abandoned cart emails sent to quote.customer_email. Data Integrity: Email is denormalized across tables for performance.
Performance Implications
Order Sync may update many rows (customer with 100s of orders). Uses collection save for efficient bulk UPDATE. Runs in same transaction as customer save (atomic).
Email Uniqueness Constraint
Email must be unique within website scope. Validated BEFORE observers run. Constraint: UNIQUE KEY (email, website_id)
Address Save Flow
Entry Point
Service Contract: AddressRepositoryInterface::save(AddressInterface $address)
Implementation: Model\ResourceModel\AddressRepository
Execution Sequence
Address Save Called
Origin: Frontend form, admin, API, checkout
Validation
- → Validate address has parent customer_id
- → Validate region matches country
- → Validate postal code format (if country requires)
- → Validate required fields (street, city, etc.)
- → Throw InputException if validation fails
Before Save Event
Event: customer_address_save_before
Observer: BeforeAddressSaveObserver validates VAT number if provided. Checks EU VAT validation service (if enabled). Sets vat_is_valid flag.
Database Persistence
- → ResourceModel\Address::save($addressModel)
- → UPDATE customer_address_entity (if existing) OR INSERT (if new)
- → UPDATE/INSERT customer_address_entity_varchar (EAV)
- → UPDATE/INSERT customer_address_entity_int, _text, etc.
- → Update updated_at timestamp
Update Default Address Flags
- → If address marked as default_billing:
- → UPDATE customer_entity SET default_billing = $addressId
- → If address marked as default_shipping:
- → UPDATE customer_entity SET default_shipping = $addressId
- → Ensures customer record points to default addresses
After Save Event - VAT Processing
Event: customer_address_save_after
Observer: AfterAddressSaveObserver - Complex VAT handling logic. If VAT validation result changed, may trigger customer group change. Updates customer_entity.group_id. Invalidates customer session/cache.
Important
Can modify customer during address save!
Key Points
VAT Validation Side Effects
BeforeAddressSaveObserver validates VAT number with EU service. AfterAddressSaveObserver may change customer group based on VAT status. CRITICAL: Address save can trigger customer save (group change). This is B2B-specific functionality.
Default Address Handling
Customer can have one default billing and one default shipping address. Flags stored in customer_address_entity.is_default_billing/shipping. Customer entity also stores FKs: default_billing, default_shipping for fast lookup.
Password Reset Flow
Entry Point
Controller: Magento\Customer\Controller\Account\ForgotPasswordPost::execute()
Route: POST /customer/account/forgotPasswordPost
Execution Sequence
User Requests Password Reset
Form data: email
Generate Reset Token
- → AccountManagementInterface::initiatePasswordReset()
- → Load customer by email + website_id
- → Generate random reset token (UUID)
- → Set expiration timestamp (default: 1 hour)
- → Update customer: rp_token = $token, rp_token_created_at = NOW()
Send Password Reset Email
Email contains link: /customer/account/createPassword?token=...&id=...
User Submits New Password
- → AccountManagementInterface::resetPassword($email, $token, $newPassword)
- → Validate token matches customer.rp_token
- → Validate token not expired (created_at + expiry)
- → Hash new password (Argon2ID13)
- → Update: password_hash = $newHash, rp_token = NULL
Key Points
Token Security
Token is random UUID (not predictable). Token stored in customer_entity.rp_token with expiration time (default 1 hour). Token invalidated (set to NULL) after successful use. One-time use only.
Security Considerations
Rate limiting on password reset requests (prevent email bombing). Token expiration prevents old emails being used. No indication if email exists (security - prevent enumeration).
Event and Observer Reference
Events Dispatched by Customer Module
| Event | When | Area |
|---|---|---|
customer_register_success |
After successful registration | frontend |
customer_save_after_data_object |
After customer DTO save | global |
customer_login |
After successful login | frontend |
customer_data_object_login |
After successful login (DTO) | frontend |
customer_logout |
After logout | frontend |
customer_customer_authenticated |
During authentication | frontend |
customer_address_save_before |
Before address save | global |
customer_address_save_after |
After address save | global |
Critical Observers in Customer Module
| Observer | Event | Side Effects |
|---|---|---|
| UpgradeOrderCustomerEmailObserver | customer_save_after_data_object | UPDATE sales_order |
| UpgradeQuoteCustomerEmailObserver | customer_save_after_data_object | UPDATE quote |
| BeforeAddressSaveObserver | customer_address_save_before | May set VAT flags |
| AfterAddressSaveObserver | customer_address_save_after | May change customer group! |
| LogLastLoginAtObserver | customer_login | UPDATE customer_log |
| Visitor\BindCustomerLoginObserver | customer_data_object_login | UPDATE customer_visitor |
| UpgradeCustomerPasswordObserver | customer_customer_authenticated | UPDATE customer_entity |