username-1.0.x-dev/modules/username_phone/username_phone.module
modules/username_phone/username_phone.module
<?php
/**
* @file
* Exposes global functionality for username phone field on user form.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Email;
use Drupal\Core\Url;
use Drupal\phonenumber\Element\Phone;
/**
* Variety of username mode options.
*
* @return array
* An array containing different username mode options.
*/
function username_phone_username_mode() {
return [
'phone' => t('Phone number'),
];
}
/**
* Implements hook_entity_type_alter().
*/
function username_phone_entity_type_alter(&$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
if (isset($entity_types['user'])) {
// Override the user entity class to use our custom class.
$entity_types['user']->setClass('Drupal\username_phone\Entity\UsernamePhoneUser');
// Set the custom form class for user registration.
$entity_types['user']->setFormClass('register', '\Drupal\username_phone\Form\UsernamePhoneForm');
}
}
/**
* Implements hook_entity_base_field_info().
*/
function username_phone_entity_base_field_info(EntityTypeInterface $entity_type) {
// Add a 'Phone' base field to the user account.
if ($entity_type->id() === 'user') {
$entity_type_id = $entity_type->id();
$fields = [];
// Define the 'phone' field.
$fields['phone'] = BaseFieldDefinition::create('string')
->setLabel(t('Phone number'))
->setDescription(t('The phone number of this user.'))
->setDefaultValue('')
->setTargetEntityTypeId($entity_type_id)
// Ensure the phone number is unique.
->addConstraint('UsernamePhoneUnique')
// Make the phone number a required field.
->addConstraint('UsernamePhoneRequired')
// Protect the field from unauthorized changes.
->addConstraint('ProtectedUserField');
return $fields;
}
}
/**
* Fetches a user object by phone number.
*
* @param string $phone
* The account's phone number.
*
* @return \Drupal\user\UserInterface|false
* A fully-loaded user object upon successful user load or FALSE if the user
* cannot be loaded.
*/
function username_phone_user_load_by_phone($phone) {
// Load users by the 'phone' property.
$users = \Drupal::entityTypeManager()->getStorage('user')
->loadByProperties(['phone' => $phone]);
// Return the first user found or FALSE if none found.
return $users ? reset($users) : FALSE;
}
/**
* Verify the phone number international format of the given phone.
*
* @param string $phone
* The user's phone number to validate international format.
*
* @return bool
* TRUE if the phone starts with a plus and digits; FALSE otherwise.
*/
function username_phone_validate_international_format($phone) {
// Check if the phone number starts with a plus sign followed by digits.
return preg_match("/^\+\d+$/", $phone) === 1;
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Alters the user login form to include phone number as a username option.
*/
function username_phone_form_user_login_form_alter(&$form, FormStateInterface $form_state) {
// Get username configuration settings.
$config_username = \Drupal::config('username.settings');
$selective = $config_username->get('selective.enabled');
$username = array_filter($config_username->get('username'));
// Return if phone is not a username.
if (!isset($username['phone'])) {
return;
}
// Get username phone configuration settings.
$config = \Drupal::config('username_phone.settings');
// Store 'phone' field parameters.
$phone_field = [
'#title' => t('Phone number'),
'#type' => 'phone',
'#phone' => [
'allow_dropdown' => $config->get('allow_dropdown'),
'show_flags' => $config->get('show_flags'),
'separate_dial_code' => $config->get('separate_dial_code'),
'countries' => $config->get('countries'),
'initial_country' => $config->get('initial_country'),
'preferred_countries' => array_keys($config->get('preferred_countries')),
'exclude_countries' => [],
'strict_mode' => TRUE,
],
'#attributes' => [
'autocorrect' => 'off',
'autocapitalize' => 'off',
'spellcheck' => 'false',
'autofocus' => 'autofocus',
],
'#required' => FALSE,
'#weight' => 0,
'#states' => [
'visible' => [
':input[name="username"]' => ['value' => 'phone'],
],
'required' => [
':input[name="username"]' => ['value' => 'phone'],
],
],
];
// Hidden fields to store phone details.
$form['phone_number'] = [
'#type' => 'hidden',
'#attributes' => ['class' => 'temporary-phone'],
];
$form['country_code'] = [
'#type' => 'hidden',
'#attributes' => ['class' => 'temporary-phone'],
];
$form['country_iso2'] = [
'#type' => 'hidden',
'#attributes' => ['class' => 'temporary-phone'],
];
// Store 'mail' field parameters.
$email_field = [
'#type' => 'email',
'#title' => t('Email address'),
'#weight' => 0,
'#required' => FALSE,
'#states' => [
'visible' => [
':input[name="username"]' => ['value' => 'mail'],
],
'required' => [
':input[name="username"]' => ['value' => 'mail'],
],
],
];
// Store 'name' field parameters.
$name_states = [
'#required' => FALSE,
'visible' => [
':input[name="username"]' => ['value' => 'name'],
],
'required' => [
':input[name="username"]' => ['value' => 'name'],
],
];
// Only phone is a username.
if (count($username) == 1) {
$form['name'] = array_merge($form['name'], $phone_field);
$form['name']['#maxlength'] = Phone::PHONE_MAX_LENGTH;
$form['name']['#required'] = TRUE;
}
// Name and phone are usernames at the same time.
if (count($username) == 2 && isset($username['name'])) {
if ($selective) {
$form['phone'] = $phone_field;
$form['name']['#states'] = $name_states;
}
}
// Mail and phone are usernames at the same time.
if (count($username) == 2 && isset($username['mail'])) {
if ($selective) {
$form['phone'] = $phone_field;
$form['name'] = array_merge($form['name'], $email_field);
}
else {
$form['name']['#maxlength'] = max(Phone::PHONE_MAX_LENGTH, Email::EMAIL_MAX_LENGTH);
}
}
// Name, mail, and phone are usernames at the same time.
if (count($username) == 3 && isset($username['name']) && isset($username['mail'])) {
if ($selective) {
$form['phone'] = $phone_field;
$form['mail'] = $email_field;
$form['name']['#states'] = $name_states;
}
else {
$form['name']['#maxlength'] = max(Phone::PHONE_MAX_LENGTH, Email::EMAIL_MAX_LENGTH);
}
}
// Add phone username element validation.
$form['name']['#element_validate'][] = 'username_phone_user_login_validate';
// Set username labels.
username_set_label($form);
// Set username placeholders.
username_set_placeholder($form);
// Set username descriptions.
username_set_description($form);
}
/**
* Form element validation handler for the user login form.
*
* Allows users to authenticate by phone number.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
function username_phone_user_login_validate(array $form, FormStateInterface $form_state) {
// Fetch the form values.
$values = $form_state->getValues();
// Get the username configuration settings.
$config = \Drupal::config('username.settings');
$usernames = array_filter($config->get('username'));
// Check if phone is enabled as a username.
if (isset($usernames['phone'])) {
// Determine which field to validate based on
// the presence of 'phone' field in form values.
$field = isset($values['phone']) ? 'phone' : 'name';
$phone = isset($values['phone']) ? $values['phone_number'] : $values['name'];
if (!empty($phone)) {
// Detect the type of the entered username (e.g., email, phone).
$type = username_detect_input_type($phone);
// Skip phone validation if input type is an
// email format and mail is enabled as username.
if (isset($usernames['mail']) && $type === 'email') {
return;
}
// Validate phone number format if core username is not active.
if (!isset($usernames['name']) && $type === 'phone' && !username_phone_validate_international_format($phone)) {
$form_state->setErrorByName(
$field,
t('Please make sure to include the "+" sign and your country code.')
);
return;
}
// Load user by phone number if any match.
if ($user = username_phone_user_load_by_phone($phone)) {
// Check if the user account is active and not blocked.
if (!$user->isActive()) {
$form_state->setErrorByName(
$field,
t('The account with the phone number %name has not been activated or is blocked.', ['%name' => $phone])
);
}
else {
// Replace the entered name with the user's
// account name to continue the default login process.
$form_state->setValue('name', $user->getAccountName());
}
}
elseif (count($usernames) == 1 || isset($values['phone'])) {
// If no user is found by phone and phone is
// the only username method, set an error.
$query = isset($form_state->getUserInput()[$field]) ? ['name' => $form_state->getUserInput()[$field]] : [];
$form_state->setErrorByName(
$field,
t('Unrecognized phone number or password. <a href=":password">Forgot your password?</a>', [
':password' => Url::fromRoute('user.pass', [], ['query' => $query])->toString(),
])
);
}
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function username_phone_form_user_pass_alter(&$form, FormStateInterface $form_state) {
// Load username configuration settings.
$config = \Drupal::config('username.settings');
$usernames = array_filter($config->get('username'));
$selective = $config->get('selective.enabled');
$override = $config->get('override');
// Return if phone is not a username.
if (!isset($usernames['phone'])) {
return;
}
// Change default name field if only phone is a username.
if (count($usernames) == 1) {
$form['name']['#title'] = t('Phone number');
if ($form['name']['#type'] === 'textfield') {
$form['name']['#type'] = 'phone';
$form['name']['#phone']['show_flags'] = TRUE;
$form['name']['#phone']['allow_dropdown'] = TRUE;
$form['name']['#phone']['separate_dial_code'] = TRUE;
$form['name']['#maxlength'] = Phone::PHONE_MAX_LENGTH;
$form['mail']['#markup'] = t('Password reset instructions will be sent to your registered phone number.');
}
}
// Change default title if override is enabled.
if (count($usernames) == 2 && isset($usernames['mail'])) {
if ($form['name']['#type'] === 'textfield') {
$form['name']['#title'] = !$selective && $override['username'] && !empty($label['username']) ? $label['username'] : t('Email address or phone number');
$form['name']['#maxlength'] = max(Phone::PHONE_MAX_LENGTH, Email::EMAIL_MAX_LENGTH);
}
}
if (count($usernames) == 3 && isset($usernames['name']) && isset($usernames['mail'])) {
if ($form['name']['#type'] === 'textfield') {
$form['name']['#title'] = !$selective && $override['username'] && !empty($label['username']) ? $label['username'] : t('Username, email address or phone number');
$form['name']['#maxlength'] = max(Phone::PHONE_MAX_LENGTH, Email::EMAIL_MAX_LENGTH);
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function username_phone_form_username_admin_settings_alter(&$form, FormStateInterface $form_state) {
// Load username phone configuration settings.
$config = \Drupal::config('username_phone.settings');
// Get list of countries.
$countries = \Drupal::service('country_manager')->getList();
// Phone setting fields.
$form['phone_settings'] = [
'#type' => 'details',
'#title' => t('Phone settings'),
'#open' => TRUE,
'#weight' => 3,
];
$form['phone_settings']['initial_country'] = [
'#type' => 'select',
'#title' => t('Default country'),
'#options' => ['auto' => t("Automatically detect user's country (geoIPLookup)")] + $countries,
'#default_value' => $config->get('initial_country'),
'#description' => t('Specify a default selection country. If geolocation is enabled, this setting will be ignored.'),
];
$form['phone_settings']['preferred_countries'] = [
'#type' => 'select',
'#title' => t('Preferred countries'),
'#multiple' => TRUE,
'#options' => $countries,
'#default_value' => $config->get('preferred_countries'),
'#description' => t('Specify the countries to appear at the top of the list. Leave it blank to follow an alphabetical order.'),
];
$form['phone_settings']['countries'] = [
'#type' => 'radios',
'#title' => t('List of countries'),
'#options' => [
'all' => t('All countries'),
'include' => t('Only the selected countries'),
'exclude' => t('Exclude the selected countries'),
],
'#default_value' => $config->get('countries'),
];
$form['phone_settings']['exclude_countries'] = [
'#type' => 'select',
'#title' => t('Excluded countries'),
'#title_display' => 'invisible',
'#options' => $countries,
'#multiple' => TRUE,
'#default_value' => $config->get('exclude_countries'),
'#states' => [
'invisible' => [
':input[name="countries"]' => ['value' => 'all'],
],
],
];
$form['phone_settings']['allow_dropdown'] = [
'#type' => 'checkbox',
'#title' => t('Allow country dropdown'),
'#default_value' => $config->get('allow_dropdown'),
];
$form['phone_settings']['separate_dial_code'] = [
'#type' => 'checkbox',
'#title' => t('Separate dial code'),
'#default_value' => $config->get('separate_dial_code'),
];
$form['phone_settings']['show_flags'] = [
'#type' => 'checkbox',
'#title' => t('Show country flags'),
'#default_value' => $config->get('show_flags'),
];
$form['phone_settings']['placeholder'] = [
'#type' => 'textfield',
'#title' => t('Placeholder text'),
'#default_value' => $config->get('placeholder'),
'#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
];
// Add username phone submit function.
$form['#submit'][] = 'username_phone_form_username_admin_settings_submit';
}
/**
* Submit function for username_admin_settings to save our variable.
*
* @see username_phone_form_user_admin_settings_alter()
*/
function username_phone_form_username_admin_settings_submit(array &$form, FormStateInterface $form_state) {
\Drupal::configFactory()->getEditable('username_phone.settings')
->set('allow_dropdown', $form_state->getValue('allow_dropdown'))
->set('initial_country', $form_state->getValue('initial_country'))
->set('preferred_countries', $form_state->getValue('preferred_countries'))
->set('separate_dial_code', $form_state->getValue('separate_dial_code'))
->set('show_flags', $form_state->getValue('show_flags'))
->set('exclude_countries', $form_state->getValue('exclude_countries'))
->set('countries', $form_state->getValue('countries'))
->save();
}
