Регистрация и авторизация пользователей на PHP. Часть четвертая

Класс для работы с массивами

В процессе обработки пользовательских данных нам всегда, так или иначе, приходится обращаться к массивам $_GET, $_POST или $_REQUEST. Что-то иногда приходится брать из массива $GLOBALS.

Просто так брать и что-то делать с пользовательскими данными достаточно... наивно, пожалуй. Когда мы берем некое значение для его дальнейшей обработки, нужно убрать из него, например, лишние пробелы, html-теги, преобразовать «опасные» символы в html-сущности и т.п. Чтобы не делать это каждый раз, когда мы обращаемся к данным, полученных из суперглобальных массивов $_GET, $_POST или $GLOBALS, мы создадим статический класс Core_Array для работы с массивами.

<?php
// Файл modules/core/array.php

defined("LEZH") || exit("Доступ к файлу запрещен");

/**
 * Класс для работы с массивами
 */
class Core_Array
{
	static protected function _filter($data)
	{
		if (empty($data))
		{
			return NULL;
		}
		return htmlspecialchars(trim($data));
	}
	
	static public function getPost($key = NULL)
	{
		if (is_null($key))
		{
			return NULL;
		}
		
		return static::get($_POST, $key);
	}
	
	static public function getGet($key = NULL)
	{
		if (is_null($key))
		{
			return NULL;
		}
		
		return static::get($_GET, $key);
	}
	
	static public function getRequest($key = NULL)
	{
		if (is_null($key))
		{
			return NULL;
		}
		
		return static::get($_REQUEST, $key);
	}
	
	static public function getSession($key = NULL)
	{
		if (is_null($key))
		{
			return NULL;
		}
		
		return static::get($_SESSION, $key);
	}
	
	static public function getCookie($key = NULL)
	{
		if (is_null($key))
		{
			return NULL;
		}
		
		return static::get($_COOKIE, $key);
	}
	
	static public function get(array $array, string $key)
	{
		if (is_null($key) || empty($array))
		{
			return NULL;
		}
		
		return (is_array($array) && array_key_exists($key, $array)) ? static::_filter($array[$key]) : NULL;
	}
	
	static public function set(array & $array, string $key, $value)
	{
		$array[$key] = $value;
	}
}

Код реализации этого класса, пожалуй, вряд ли требует какого-то особого описания. Читатель должен всё хорошо понять.

Порядок работы с формой регистрации пользователя

Возвращаемся к работе над формой регистрации пользователя. Что произойдет, если её отправить? Каждое из полей формы имеет атрибут required. Это не позволит отправить форму до тех пор, пока все поля с этим атрибутом не будут заполнены. Но, допустим, мы их заполнили. Что дальше?

Дальше требуется проверить, доказал ли пользователь, что он не робот. Все данные формы, в том числе и поле капчи, проверять и обрабатывать мы будем при помощи JavaScript, а точнее — AJAX.

Начнем с обработки поля капчи. Вероятно, нашей форме не хватает поля для вывода сообщений пользователю. Добавим его над самой формой.

<?php
// Файл registration.php

require_once('bootstrap.php');

// Получаем параметры конфигурации для генерации нашей капчи
$oCaptcha_Config = Core_Config::instance()->get("captcha");
?>
<!doctype html>
<html>
	<head>
		<title>Регистрация нового пользователя</title>
		<?php
		// Подключаем необходимые CSS-файлы
		require_once(INCLUDE_BLOCKS_PATH . "css.php");
		
		// Подключаем необходимые JS-файлы сценариев
		require_once(INCLUDE_BLOCKS_PATH . "scripts.php");
		?>
	</head>
	<body>
		
		<?php
		// Подключаем блок header для страницы
		require_once(INCLUDE_BLOCKS_PATH . "header.php");
		?>
		
		<main>
			<div class="container-fluid container-lg">
				
				<h1>Регистрация пользователя сайта</h1>
				
				<div id="user-message"></div>
				
				<div id="user-registration-form">
					
					<!-- Все поля формы обязательны для заполнения -->
					<form id="user-registration" name="user-registration" action="/service.php" autocomplete="off" method="post" onsubmit="return false;">
						
						<!-- Поле формы -->
						<div class="form-group row">
							<label for="user_login" class="col-md-3 col-form-label">Ваш логин:</label>
							<div class="col-md-9">
								<!-- Непосредственно само поле -->
								<input type="email" class="form-control" id="user_login" name="user_login" placeholder="Например: evgeni@lezhenkin.ru" required />
								<!-- Описание поля формы и требования к его заполнению -->
								<div class="alert alert-info instr" id="user_login-instr">Укажите ваш действительный адрес электропочты, к которому вы имеете доступ, и на котором вы можете принимать и отправлять письма.</div>
								<!-- Поле для вывода сообщения об ошибке, относящегося именно к этому полю формы -->
								<div class="alert alert-danger error" id="user_login-error"></div>
							</div>
						</div>
						
						<!-- Поле формы -->
						<div class="form-group row">
							<label for="user_password" class="col-md-3 col-form-label">Ваш пароль:</label>
							<div class="col-md-9">
								<!-- Непосредственно само поле -->
								<input type="password" min="5" max="32" class="form-control" id="user_password" name="user_password" required />
								<!-- Описание поля формы и требования к его заполнению -->
								<div class="alert alert-info instr" id="user_password-instr">Пароль может состоять только из символов латинского алфавита, цифр, и символов: <strong><samp>* - + ! @ # $ [ ] { } ' " ; : > < , .</samp></strong>. Пароль дожен содержать, как минимум, одну заглавную букву, и, как минимум, одну цифру. Длина пароля не должна быть менее шести символов, и не должна превышать 32 символа.</div>
								<!-- Поле для вывода сообщения об ошибке, относящегося именно к этому полю формы -->
								<div class="alert alert-danger error" id="user_password-error"></div>
							</div>
						</div>
						
						<!-- Поле формы -->
						<div class="form-group row">
							<label for="user_password_approve" class="col-md-3 col-form-label">Подтвердите введенный пароль:</label>
							<div class="col-md-9">
								<!-- Непосредственно само поле -->
								<input type="password" class="form-control" id="user_password_approve" name="user_password_approve" required />
								<!-- Описание поля формы и требования к его заполнению -->
								<div class="alert alert-info instr" id="user_password_approve-instr">Вы должны ввести одинаковые пароли</div>
								<!-- Поле для вывода сообщения об ошибке, относящегося именно к этому полю формы -->
								<div class="alert alert-danger error" id="user_password_approve-error"></div>
							</div>
						</div>
						
						<!-- Капча Google recaptcha -->
						<div class="form-group row">
							<label for="user_captcha" class="col-md-3 col-form-label">Вы не робот?</label>
							<div class="col-md-9">
								<!-- Чтобы заработала капча Google, нужно лишь добавить одно поле -->
								<div class="g-recaptcha" data-sitekey="<?=$oCaptcha_Config["site_key"]?>"></div>
							</div>
						</div>
						
						<!-- Скрытое поле формы, в котором хранится строка запроса к сценарию обработчику -->
						<input type="hidden" id="user_registration_request" name="request" value="user_registration" />
						
						<!-- Кнопки управления формой -->
						<div class="btn-group offset-md-3">
							
							
							<input type="submit" name="submit" value="Зарегистрироваться" class="ml-md-2 btn btn-success" />
							
							<input type="reset" name="reset" value="Очистить" class="btn btn-warning" />
							
							<a href="/" class="btn btn-danger">На главную</a>
							
						</div>
						
					</form>
					
				</div>
				
			</div>
		</main>
		
	</body>
</html>

В контейнер с ID user-message мы будем помещать все сообщения для пользователя. Общая схема взаимодействия пользователя и формы будет такая:

  • пользователь что-то сделал с полями формы, или не сделал вообще ничего;
  • пользователь нажал кнопку отправки формы или кнопки сброса, возврат;
  • если пользователь отправил форму, JavaScript сначала проверяет корректность капчи;
  • если капча корректная, JavaScript берет все имеющиеся поля формы, проверяет данные на соответствие требованиям;
  • если все данные формы верны, JavaScript передает их серверу на обработку, ожидая от него ответ.

Пользовательские функции JavaScript для формы регистрации пользователя

Нам потребуется добавить множество пользовательских функций в сценарий JavaScript. Вынесем их все в отдельный файл. Дополним код нашего файла /blocks/scripts.php.

<!-- Файл blocks/scripts.php -->

<!-- jQuery -->
<script src="https://yastatic.net/jquery/3.3.1/jquery.min.js"></script>
<!-- JS Bootstrap 4 -->
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
<!-- reCAPTCHA Google -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<!-- Пользовательские JS-сценарии -->
<script src="/js/func.js"></script>
<script src="/js/scripts.js"></script>

Обработчик формы регистрации пользователя на сайте

Добавляем обработчик формы регистрации пользователя на сайте, который будет срабатывать при нажатии кнопки отправки формы.

// Файл /js/scripts.js
// Используем строгий режим ECMAScript 5
"use strict";

(function($) {
	
	jQuery(function() {
		
		// Обработчик события отправки формы регистрации пользователя на сайте
		$("#user-registration").on("submit", $.userRegistrationHandler)
		
		// Добавляем обработчик на поля формы
		// Если поле формы в фокусе, очищаем его от ошибки
		.find("input:text, input:password, input[type='email']")
		.on("focus", $.cleanError);
		
	});
	
})(jQuery);

Сам же метод $.userRegistrationHandler() мы объявим в нашем новом файле /js/func.js.

// Файл /js/func.js
// Используем строгий режим ECMAScript 5
"use strict";

$.extend({
	
	userRegistrationHandler: function()
	{
		alert("Обработка формы регистрации пользователя на сайте");
	}
	
});

(function($) {
	
	jQuery(function() {
		
		
		
	});
	
})(jQuery);

Проверка капчи

Как уже было сказано выше, первым делом следует проверить корректность капчи. Этим и займёмся. В качестве капчи мы используем Google reCAPTCHA.

Автор не описывает читателю почему он использует строгий режим ECMAScript 5. Читатель вправе самостоятельно изучить информацию об этом режиме работы JavaScript.

Метод $.userRegistrationHandler() должен проверить корректность капчи, и только в случае положительного результата проверки отправлять данные форму на обработку.

/**
 * Обработывает форму регистрации пользователя на сайте по событию отправки формы
*/
userRegistrationHandler: function()
{
	var form = $( this ), // Сама форма регистрации пользователя на сайте
		login = $("#user_login").val(), // Логин пользователя
		password = $("#user_password").val(), // Пароль пользователя
		password_approve = $("#user_password_approve").val(), // Подтвержденный пароль пользователя
		sCaptcha = form.find(".g-recaptcha-response").val(), // Значение капчи Google reCAPTCHA
		checkPassword, 
		checkLogin;
	
	// Сначала проверяем капчу
	// Продолжаем обработку формы только в случае корректной капчи
	if ($.checkCapctha(sCaptcha, form.attr("action")))
	{
		// Очищаем все поля ошибок
		$.cleanAllErrors(this);
	
		// Если введенные пароли не идентичны, прекращаем обработку
		if (password !== password_approve)
		{
			$.showError("user_password_approve", "Введенные вами пароли не совпадают");
			
			// Помечаем поле как проверенное
			$.validField("user_password_approve", false);
			
			return false;
		}
		else
		{
			// Помечаем поле как проверенное
			$.validField("user_password_approve", true);
		}
		
		// Проверяем пароль на соответствие требованиям
		// Если он не подходит, прекращаем работу
		checkPassword = $.checkPassword(password);
		if (!checkPassword.success)
		{
			$.showError("user_password", checkPassword.message);
			
			// Помечаем поле как проверенное
			$.validField("user_password", false);
			
			return false;
		}
		else
		{
			// Помечаем поле как проверенное
			$.validField("user_password", true);
		}
		
		// Проверяем логин пользователя, являющийся одновременно и адресом электропочты
		// Если он не прошел проверку, прекращаем проверку
		checkLogin = $.checkLogin(login, form.attr("action"));
		if (!checkLogin.success)
		{
			$.showError("user_login", checkLogin.message);
			
			// Помечаем поле как проверенное
			$.validField("user_login", false);
			
			return false;
		}
		else 
		{
			// Помечаем поле как проверенное
			$.validField("user_login", true);
		}
		
		// Если нет ошибок, регистрируем пользователя
		if ($("#user-registration .error:visible").length == 0)
		{
			var request = {};
			
			// Формируем объект данных, который будет передан AJAX-запросом
			request.data = {
				request: "userRegistration",
				login: login,
				password: password,
				password_approve: password_approve
			};
			
			// URL для AJAX-запроса
			request.url = $("#user-registration").attr("action");
			
			// Запрос будет синхронным
			request.async = false;
			
			// Действия после выполнения запроса
			request.success = function(data, textStatus, jqXHR )
			{
				// Сохраняем результат выполнения запроса
				$.response.data = data;
				$.response.textStatus = textStatus;
				$.response.jqXHR = jqXHR;
				
				// Если запрос выполнен успешно
				if (typeof (data) != "undefined")
				{
					// Если при регистрации что-то пошло не так
					if (data.success === false)
					{
						showError("user_message", data.message);
					}
					else 
					{
						// Очищаем поля формы
						$("#user-registration").trigger("reset");
						
						alert(data.message + "\nСейчас вы будете перенаправлены на главную страницу сайта");
						
						window.location.href = "/";
					}
				}
			};
			
			request = $.extend({}, $.request, request);
			
			// Выполняем запрос
			$.ajax(request);
		}
		else
		{
			alert("При заполнении полей формы были найдены ошибки. Пожалуйста, исправьте.");
		}
		
	}
	// Если капча была заполнена некорректно, показываем ошибку
	else
	{
		alert("Вы не подтвердили, что вы не робот. Ответьте на вопрос, пожалуйста.");
		
		// Больше ничего не делаем
		return false;
	}
}

Вероятно, читатель не ожидал увидеть так много всего. А что же это за всякие: $.checkCaptcha, $.cleanAllErrors, $.showError, $.validFields, $.checkPassword, $.checkLogin? Это методы, которые занимаются проверкой введенных пользователем данных.

Этапы проверки данных и последующая регистрация пользователя

Настало время описать словами всё, что делает метод $.userRegistrationHandler.

  1. Инициализируются все необходимые переменные.
  2. Проверяется корректность капчи Google reCAPTCHA:
    • если капча корректная, обработка данных продолжается;
    • если капча некорректная, дальше ничего не делаем, сообщаем об этом пользователю.
  3. Итак. Капча корректная. Убираем все ошибки полей формы, сбрасываем их состояние до непроверенных (метод $.cleanAllErrors):.
    • если методу не была передана форма, ничего не делаем, возвращаем false;
    • скрываем все поля ошибок внутри формы, убираем тексты ошибок;
    • ищем текстовые поля формы, снимаем с них пометки проверки;
    • если есть поле для сообщений пользователю, очищаем и его;
    • сбрасываем капчу.
  4. Если введенные пользователем пароли не идентичны, показываем ошибку. В ином случае помечаем поле корректным.
  5. Если пароль не соответствует требованиям, показываем ошибку. В ином случае помечаем поле корректным.
  6. Если логин пользователя не признан корректным, а он у нас является адресом электропочты, показываем ошибку. В ином случае помечаем поле корректным. На этом же шаге проверяется уникальность логина.
  7. Если нет отображенных ошибок, можем регистрировать пользователя.
  8. Если регистрация была успешной, отправляем пользователя на главную страницу сайта, а на сайте он уже будет авторизован.

Вот и всё, в общем-то. Ну, это мы увидели JavaScript-сторону процесса. И она, к тому же, неполная. Весь файл func.js выглядит следующим образом.

// Файл /js/func.js
// Используем строгий режим ECMAScript 5
"use strict";

$.extend({

	/**
	 * Параметры по умолчанию для AJAX-запросов
	*/
	request: 
	{
		// Путь для запросов
		url: "",
		// Метод запросов
		type: "POST",
		// Тип данных, которые ожидаются в ответ от сервера
		dataType: "json",
		// Действия, выполняемые при неуспешных запросах
		error: function(jqXHR, textStatus, errorThrown)
		{
			// Пишем текст ошибки в консоль
			console.log("textStatus: " + textStatus + ". Error: " + errorThrown);
			// Выводим ошибку на экран
			alert("textStatus: " + textStatus + ". Error: " + errorThrown);
		}
	},
	
	/**
	 * Объект для хранения результатов AJAX-запросов
	*/
	response: 
	{
		data: null,
		textStatus: null,
		jqXHR: null
	},
	
	/**
	 * Очищает все поля формы от сообщений об ошибках
	*/
	cleanAllErrors: function(form)
	{
		// Если не передана форма, ничего не делаем
		if (!form)
		{
			return false;
		}
		
		// Внутри формы ищем все поля для сообщений об ошибке, очищаем их и убираем с экрана
		$( form ).find(".error").hide().text("")
		
		// Для всех полей формы убираем классы валидации
		.end()
		.find("input:text, input:password, input[type='email']")
		.removeClass("is-valid").removeClass("is-invalid");
		
		// Если есть поле для сообщения пользователю
		if ($("#user-message").length)
		{
			// Очищаем и его
			$("#user-message").html("");
		}
		
		// Сбрасываем капчу
		if (typeof (grecaptcha) != "undefined" && typeof (grecaptcha.reset) == "function")
		{
			grecaptcha.reset();
		}
	},
	
	/**
	 * Очищает поле формы от ошибки
	*/
	cleanError: function(event)
	{
		// Если не передано поле, ничего не делаем
		if (!event)
		{
			return false;
		}
		
		// Сохраняем идентификатор поля формы
		var field = event.currentTarget, id = $( field ).attr("id");
		
		// Очищаем у поля классы валидации
		$( field ).removeClass("is-valid").removeClass("is-invalid");
		
		// Очищаем поле ошибки, если оно есть
		$("#" + id + "-error").hide().text("");
	},
	
	/**
	 * Показывает ошибку для поля формы
	*/
	showError: function(sFieldId, sMessage)
	{
		// Если не передан идентификатор поля, ничего не делаем
		if (!sFieldId)
		{
			console.log("Не передан идентификатор поля формы");
			return false;
		}
		
		var field = $("#" + sFieldId),	// Поле формы
			// Поле для сообщения об ошибке
			errorField = $("#" + sFieldId + "-error"),
			// Текст сообщения об ошибке по умолчанию
			sMessage = sMessage || "Поле заполнено неверно. Проверьте";
		
		// Показываем сообщение об ошибке
		field.addClass("is-invalid");
		errorField.text(sMessage).show();
	},
	
	/**
	 * Помечает поле указанным способом 
	*/
	validField: function(fieldId, validType)
	{
		// Результат проверки по-умолчанию
		validType = validType || true;
		
		if (!fieldId)
		{
			console.log("Не указано поле, которое необходимо отметить как проверенное");
			alert("Не указано поле, которое необходимо отметить как проверенное");
			
			return false;
		}
		
		var className = (validType === true) ? "is-valid" : "is-invalid";
		
		$("#" + fieldId).addClass(className);
	},
	
	/**
	 * Проверяет корректность капчи
	*/
	checkCapctha: function(sCaptcha, url)
	{
		// Если нет капчи, проверка не может быть успешной
		if (!sCaptcha)
		{
			return false;
		}
		
		// URL для запроса
		url = url || "";
		
		// Подготавливаем данные для AJAX-запроса
		var request = {};
		
		// Строка AJAX-запроса
		request.data = "request=checkCaptcha&captchaValue=" + sCaptcha;
		
		// URL для AJAX-запроса
		request.url = url;
		
		// Запрос будет синхронный
		request.async = false;
		
		// Действия после выполнения запроса
		request.success = function(data, textStatus, jqXHR )
		{
			// Сохраняем результат выполнения запроса
			$.response.data = data;
			$.response.textStatus = textStatus;
			$.response.jqXHR = jqXHR;
		};
		
		request = $.extend({}, $.request, request);
		
		// Выполняем запрос
		$.ajax(request);
		
		// Возвращаем результат проверки капчи
		return $.response.data.success;
	},
	
	/**
	 * Проверяет пароль на соответствие требованиям
	*/
	checkPassword: function(sPassword)
	{
		var response =
			{
				success: false,
				message: "Пароль для проверки не передан"
			},
			pattern1 = {},
			pattern2 = {},
			pattern3 = {};
		
		// Если не передан пароль
		if (!sPassword)
		{
			return response;
		}
		
		/**
		 * Нужно проверить пароль на соответствие нескольким требованиям
		 * 
		 * 1. Длина пароля не менее 6 и не более 32 символов
		 * 2. Пароль не может содержать символы кроме символов латинского алфавита и *-+!@#$[]{}'";:><,.
		 * 3. Пароль доджен содержать как минимум одну цифру, и как минимум одну заглавную букву
		 * 
		 * Для каждого из условий добавляем проверку
		*/
		// Для указанного щаблона регулярного выражения...
		pattern1.pattern = /^.{6,32}$/;
		// ...нужен положительный результат
		pattern1.result = true;
		
		// Для указанного щаблона регулярного выражения...
		pattern2.pattern = /[^-\w\d*+!@#$\[\]\{\}'";:><,.]/;
		// ...нужен отрицательный результат
		pattern2.result = false;
		
		// Для указанного щаблона регулярного выражения...
		pattern3.pattern = /([A-Z].+?\d|\d.+?[A-Z])/;
		// ...нужен положительный результат
		pattern3.result = true;
		
		if (pattern1.pattern.test(sPassword) !== pattern1.result)
		{
			response.message = "Пароль не соответствует требованиям по длине";
			
			return response;
		}
		else if (pattern2.pattern.test(sPassword) !== pattern2.result)
		{
			response.message = "Пароль содержит недопустимые символы";
			
			return response;
		}
		else if (pattern3.pattern.test(sPassword) !== pattern3.result)
		{
			response.message = "Пароль не содержит в себе обязательные симовлы";
			
			return response;
		}
		// Если все проверки прошли
		else
		{
			response.success = true;
			response.message = "Пароль соответствует требованиям";
		}
		
		return response;
	},
	
	/**
	 * Проверяет логин пользователя
	 * 
	 * Проверка логина состоит из двух этапов.
	 *
	 * 1. Проверяем корректность указанного адреса электропочты
	 * 2. Проверяем уникальность указанного адреса электропочты
	*/
	checkLogin: function(email, url)
	{
		var response = 
			{
				success: false,
				message: "Не удалось проверить логин пользователя"
			},
			request = {};
			
		// Путь для выполнения AJAX-запросов по умолчанию
		url = url || "";
		
		if (!email)
		{
			console.log("Не передано значение логина пользователя для проверки");
			response.message = "Не передано значение логина пользователя для проверки";
			
			return response;
		}
		
		var pattern = /^[-_.\da-z]+@[-.a-z\d]{1,}\.(ru|com|su|[a-z]{2,4})$/i;
		
		// Убеждаемся в корректности адреса электропочты
		if (!pattern.test(email))
		{
			response.message = "Некорректный формат адреса электропочты";
			
			return response;
		}
		
		// Проверяем уникальность логина пользователя
		request.data = "request=checkLoginExists&email=" + email;
		
		// URL для AJAX-запроса
		request.url = url;
		
		// Запрос будет синхронным
		request.async = false;
		
		// Действия после выполнения запроса
		request.success = function(data, textStatus, jqXHR )
		{
			// Сохраняем результат выполнения запроса
			$.response.data = data;
			$.response.textStatus = textStatus;
			$.response.jqXHR = jqXHR;
			
			// Если запрос выполнен успешно
			if (typeof (data) != "undefined")
			{
				// Если email не уникален
				if (data.success === false)
				{
					response.message = data.message;
				}
				else 
				{
					response.success = true;
					response.message = "Указанный логин доступен для регистрации";
				}
			}
		};
		
		request = $.extend({}, $.request, request);
		
		// Выполняем запрос
		$.ajax(request);
		
		return response;
	},
	
	/**
	 * Обработывает форму регистрации пользователя на сайте по событию отправки формы
	*/
	userRegistrationHandler: function()
	{
		var form = $( this ), // Сама форма регистрации пользователя на сайте
			login = $("#user_login").val(), // Логин пользователя
			password = $("#user_password").val(), // Пароль пользователя
			password_approve = $("#user_password_approve").val(), // Подтвержденный пароль пользователя
			sCaptcha = form.find(".g-recaptcha-response").val(), // Значение капчи Google reCAPTCHA
			checkPassword, 
			checkLogin;
		
		// Сначала проверяем капчу
		// Продолжаем обработку формы только в случае корректной капчи
		if ($.checkCapctha(sCaptcha, form.attr("action")))
		{
			// Очищаем все поля ошибок
			$.cleanAllErrors(this);
		
			// Если введенные пароли не идентичны, прекращаем обработку
			if (password !== password_approve)
			{
				$.showError("user_password_approve", "Введенные вами пароли не совпадают");
				
				// Помечаем поле как проверенное
				$.validField("user_password_approve", false);
				
				return false;
			}
			else
			{
				// Помечаем поле как проверенное
				$.validField("user_password_approve", true);
			}
			
			// Проверяем пароль на соответствие требованиям
			// Если он не подходит, прекращаем работу
			checkPassword = $.checkPassword(password);
			if (!checkPassword.success)
			{
				$.showError("user_password", checkPassword.message);
				
				// Помечаем поле как проверенное
				$.validField("user_password", false);
				
				return false;
			}
			else
			{
				// Помечаем поле как проверенное
				$.validField("user_password", true);
			}
			
			// Проверяем логин пользователя, являющийся одновременно и адресом электропочты
			// Если он не прошел проверку, прекращаем проверку
			checkLogin = $.checkLogin(login, form.attr("action"));
			if (!checkLogin.success)
			{
				$.showError("user_login", checkLogin.message);
				
				// Помечаем поле как проверенное
				$.validField("user_login", false);
				
				return false;
			}
			else 
			{
				// Помечаем поле как проверенное
				$.validField("user_login", true);
			}
			
			// Если нет ошибок, регистрируем пользователя
			if ($("#user-registration .error:visible").length == 0)
			{
				var request = {};
				
				// Формируем объект данных, который будет передан AJAX-запросом
				request.data = {
					request: "userRegistration",
					login: login,
					password: password,
					password_approve: password_approve
				};
				
				// URL для AJAX-запроса
				request.url = $("#user-registration").attr("action");
				
				// Запрос будет синхронным
				request.async = false;
				
				// Действия после выполнения запроса
				request.success = function(data, textStatus, jqXHR )
				{
					// Сохраняем результат выполнения запроса
					$.response.data = data;
					$.response.textStatus = textStatus;
					$.response.jqXHR = jqXHR;
					
					// Если запрос выполнен успешно
					if (typeof (data) != "undefined")
					{
						// Если при регистрации что-то пошло не так
						if (data.success === false)
						{
							showError("user_message", data.message);
						}
						else 
						{
							// Очищаем поля формы
							$("#user-registration").trigger("reset");
							
							alert(data.message + "\nСейчас вы будете перенаправлены на главную страницу сайта");
							
							window.location.href = "/";
						}
					}
				};
				
				request = $.extend({}, $.request, request);
				
				// Выполняем запрос
				$.ajax(request);
			}
			else
			{
				alert("При заполнении полей формы были найдены ошибки. Пожалуйста, исправьте.");
			}
			
		}
		// Если капча была заполнена некорректно, показываем ошибку
		else
		{
			alert("Вы не подтвердили, что вы не робот. Ответьте на вопрос, пожалуйста.");
			
			// Больше ничего не делаем
			return false;
		}
	}
	
});

(function($) {
	
	jQuery(function() {
		
		
	});
	
})(jQuery);
Автор не видит необходимости в подробном описании приведенного кода файла func.js. Автор постарался наполнить код этого файла достаточно обширными по смыслу комментариями, чтобы читатель при просмотре понимал, как всё происходит.

Что же, проверка капчи, проверка уникальности логина и сама регистрация происходят через AJAX-запросы. Они отправляются в сценарий по адресу /service.php. Давайте посмотрим, что есть в файле /service.php.

Сценарий PHP для обработки AJAX-запросов

<?php
// Файл service.php
// Инициализируем ядро сайта
require_once("bootstrap.php");

// Делаем что-либо только если выполняется запрос
if (!empty($_REQUEST) && !empty(Core_Array::getRequest("request")))
{
	// По умолчанию данные передаём в формате JSON
	$bJson = TRUE;
	
	// По умолчанию прерываем работу сценария
	$bExit = TRUE;
	
	// Данные, отправляемые в ответ на запрос по умолчанию
	$response = array(
		"success" => FALSE,
		"message" => "При выполнении запроса что-то пошло не так"
	);
	
	// Сохраняем строку значения типа запроса
	$sRequest = strval(Core_Array::getRequest("request"));
	
	// Для разных типов запроса разные действия
	switch ($sRequest)
	{
		// Проверка корректности капчи Google reCAPTCHA
		case "checkCaptcha":
			
			// Сохраняем полученное значение капчи
			$sCaptcha = strval(Core_Array::getRequest("captchaValue"));
			
			// Получаем параметры конфигурации для Google reCAPTCHA
			$aCaptcha_Config = Core_Config::instance()->get("captcha");
			
			// Формируем данные для передачи в запросе
			$aData = array(
				"secret" => $aCaptcha_Config["secret_key"],
				"response" => $sCaptcha
			);
			
			// Запрос к серверу Google выполним через cURL
			$curl = curl_init();
			
			// Указываем параметры запроса
			curl_setopt($curl, CURLOPT_URL, $aCaptcha_Config["url_verify"]);
			// Нам нужен возврат результата, а не вывод в браузер
			curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
			// Будем ли передавать заголовки
			curl_setopt($curl, CURLOPT_HEADER, FALSE);
			// Подставляем передаваемые данные
			curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($aData));
			
			// Выполняем запрос и сохраняем его результат
			$sResponse = curl_exec($curl);
			
			// Преобразуем ответ в массив
			$response = json_decode($sResponse, TRUE);
			
		break;
		
		// Проверка уникальности логина пользователя
		case "checkLoginExists":
			
			// Сохраняем переданное значение логина
			$sEmail = strval(Core_Array::getRequest("email"));
			
			// Пробуем найти пользователя по указанному логину
			$oUser = Core::factory("User")
				->getByLogin($sEmail);
				
			// Если такого пользователя нет
			if (is_null($oUser))
			{
				$response["success"] = TRUE;
				$response["message"] = "Указанный вами логин доступен для регистрации";
			}
			// Если такой логин в системе уже зарегистрирован
			else
			{
				$response["message"] = "Пользователь с указанным логином уже зарегистрирован";
			}
			
		break;
		
		// Регистрация нового пользователя
		case "userRegistration":
			
			// Сохраняем переданные данные 
			$login = strval(Core_Array::getRequest("login"));
			$password = strval(Core_Array::getRequest("password"));
			$password_approve = strval(Core_Array::getRequest("password_approve"));
			
			// Создаем объект модели пользователя
			$oUser = Core::factory("User");
			
			// Создаем объект контроллера пользователя
			$User_Controller = new User_Controller($oUser);
			
			// Добавляем в контроллер полученные данные
			$User_Controller->setLogin($login)
				->setPassword($password)
				->setPasswordApprove($password_approve);
			
			// Сначала проверяем все переданные данные
			// Если данные не прошли проверку, больше ничего не делаем
			if (!$User_Controller->checkRegistrationData())
			{
				$response["message"] = "Переданные на регистрацию данные о пользователе не прошли проверку.";
				
				break;
			}
			
			// Регистрируем пользователя, сохраняя его в БД
			$iUserId = $User_Controller->registration();
			
			// Если пользователь успешно сохранен
			if (!is_null($iUserId))
			{
				// Авторизуем на сайте нового пользователя сразу же
				$oUser->authUser();
				
				/**
				 * Это может показаться странным, но методу authUser() действительно
				 * не передаётся никакой идентификатор. В объект модели уже были 
				 * загружены необходимые данные пользователя в процессе его регистрации
				 */
				
				$response["success"] = TRUE;
				$response["message"] = "Пользователь с логином " . $oUser->login . " успешно зарегистрирован";
			}
			else
			{
				$response["message"] = "Не удалось зарегистрировать пользователя с логином " . $login;
			}
			
		break;
	}
	
	// Если требуется представить данные в формате JSON
	$bJson && print json_encode($response);
	
	// Если требуется прервать выполнение скрипта
	$bExit && exit();
}
else
{
	exit("Доступ к разделу запрещен");
}
?>

В целом всё должны быть понятно и очевидно. При проверке капчи с помощью cURL выполняется запрос к серверу Google, который присылает нам ответ. Мы ничего не делаем с телом ответа, просто передаём его, в свою очередь, в ответ на AJAX-запрос проверки капчи. И там уже JavaScript-сценарий определяет дальнейшие действия на основании содержимого ответа от Google.

Проверка уникальности логина тоже не самая сложная. Но для читателя здесь новым окажется метод: ->getByLogin() объекта модели пользователя сайта. Этот метод объект модели пользователя наследует от класса Core_ORM через реализацию в нём магического метода __call(). То есть может быть множество иных методов с именами типа: getByPassword(), getByBirthday() и еще любые иные.

Код реализации управления регистрацией пользователя не очень обширный, но для читателя здесь тоже появляется нечто новое: контроллер объекта модели пользователя. С помощью модели мы лишь получаем или редактируем данные, а вот манипулировать ими будем с помощью контроллера.

Несмотря на то, что данные для регистрации были проверены еще JavaScript-сценарием, мы не поленимся проверить их вновь. Если будут ошибки, работа сценария прервется. А в итоге успешно зарегистрированный пользователь будет авторизован на сайте.

Итак, приводим пример реализации вышеописанного кода. Первое, что нужно показать, это реализация работы метода getByLogin(). За это отвечает класс Core_ORM.

Автор постарался максимально полно и осмысленно комментировать код в приведенных примерах, поэтому отдельных пояснений к показанному дано не будет.

Реализация работы метода getByLogin()

Читателя здесь заинтересует то, что написано для метода __call().

<?php
// Файл modules/core/orm.php

defined("LEZH") || exit("Доступ к файлу запрещен");

class Core_ORM
{
	// Здесь будет храниться экземпляр объекта класса работы с СУБД
	protected $_oDatabase = NULL;
	
	// Имя таблицы для запросов
	protected $_tableName = NULL;
	
	// Имена полей таблицы
	protected $_columnNames = array();
	
	// Объект запроса к СУБД
	protected $_oQuery = NULL;
	
	// Первичный ключ таблицы, его значение
	protected $_primaryKey = NULL;
	
	// Статус загрузки данных модели из БД
	protected $_loaded = FALSE;
	
	// Данные модели, загруженные из БД
	protected $_modelColumns = array();
	
	// Перечень полей модели, запрещенных к загрузке
	protected $_forbiddenFields = array();
	
	// Поля, требующие сохранения значений
	protected $_modelFieldsValues = array();
	
	public function __construct($iPrimaryKey)
	{
		// Подключаемся к СУБД
		$this->_oDatabase = Core_Database::instance();
		
		// Получаем имя таблицы, к которой будет выполняться запрос
		// Таблица будет совпадать либо с именем класса, либо будет 
		// указана в свойстве _modelName дочернего класса
		// Если имя таблицы не указано в свойстве класса
		if (empty($this->_modelName))
		{
			/**
			 * Модели могут называться по-разному, но в базе данных таблицы 
			 * будут иметь названия во множественном числе
			 * 
			 * Например, модель User — таблица users
			 * модель Wireless — таблица wirelesses
			 */
			 $this->_tableName = $this->_getModelName();
		}
		// Если оно указано в свойстве класса
		else
		{
			$this->_tableName = $this->_modelName;
		}
		
		// Подключаемся к таблице, получаем перечень её полей
		$this->_getModelColumnNames();
		
		// Если был передан первичный ключ модели, ввыбираем данные по нему
		if (!empty($iPrimaryKey))
		{
			$this->setPrimaryKey($iPrimaryKey);
			
			$this->_load();
		}
		
		// Объект готов к работе
	}
	
	/**
	 * Получает имя модели из наименования класса
	 */
	protected function _getModelName()
	{
		// Получаем имя класса
		$sClassName = get_class($this);
		
		// Разбираем имя класса
		$aClassName = explode("_", $sClassName);
		
		// Если массив пустой, значит в имени было только одно слово
		// Если имя класса состояло из нескольких слов, нам нужно последнее
		if (!empty($aClassName))
		{
			$sClassName = array_pop($aClassName);
			
			// Если последним элементом было слово Model, отбрасываем его
			$sClassName = array_pop($aClassName);
		}
		
		// Определяем окончание имени класса
		// Если окончание является одним из: —s, —ss, —sh, —ch, —tch, —z, —x
		if (preg_match("/^(?<word>.+[^s])(?<ending>ss?|sh|t?ch|z|x)$/Dsui", $sClassName, $matches))
		{
			// Если определено окончание 
			if (!empty($matches["ending"]))
			{
				// К имени класса просто добавляем -es
				$sClassName .= "es";
			}
		}
		// Если окончанием является —y
		elseif (preg_match("/^(?<word>.+)(?<ending>y)$/Dsui", $sClassName, $matches))
		{
			// Если определено окончание 
			if (!empty($matches["ending"]))
			{
				// Отбрасывем окончание, к слову добавляем -ies
				$sClassName = $matches["word"] . "ies";
			}
		}
		// В иных случаях добавляем к слову имени класса -s
		else
		{
			$sClassName .= "s";
		}
		
		return strtolower($sClassName);
	}
	
	/**
	 * Получает имена полей таблицы в БД
	 */
	protected function _getModelColumnNames()
	{
		$this->_columnNames = $this->getColumnNames($this->_tableName);
		
		return $this;
	}
	
	public function getColumnNames($tableName)
	{
		return $this->query()
			->getColumnNames($tableName);
	}
	
	/**
	 * Загружает данные в модель по конкретному значению первичного ключа
	 */
	public function find(int $iPrimaryKey)
	{
		$this->_load($iPrimaryKey);
		
		return $this;
	}
	
	/**
	 * Загружает данные модели из БД
	 */
	protected function _load($iPrimaryKey = NULL)
	{
		// Переключаем статус загрузки данных модели из БД
		$this->_loaded = FALSE;
		
		// Загружаем данные
		$oQuery = $this->query()
					->select()
					->from($this->_tableName)
					->asAssoc();
		
		// Получаем имя поля, являющимся первичным ключом
		$sFieldName = $this->_columnNames["primary"];
		
		// Если в модели уже есть заданное значение первичного ключа
		if (!is_null($this->_primaryKey))
		{
			$oQuery->where($sFieldName, "=", $this->_primaryKey);
		}
		// Если был передан конкретный идентификатор 
		elseif (!is_null($iPrimaryKey))
		{
			// Добавляем условие отбора данных
			$oQuery->where($sFieldName, "=", $iPrimaryKey);
		}
		// Иначе загружать нечего
		else 
		{
			return NULL;
		}
		
		$oQuery->execute();
					
		$aLoadedValues = $oQuery->fetchAll()[0];
		
		// Перебираем значения, ищем запрещенные к загрузке
		if (!empty($this->_forbiddenFields))
		{
			// Для каждого из таких запрещенных полей
			foreach ($this->_forbiddenFields as $sFieldName)
			{
				// Если в загруженных результатах есть такое поле
				if (array_key_exists($sFieldName, $aLoadedValues))
				{
					// Убираем его
					unset($aLoadedValues[$sFieldName]);
				}
			}
		}
		
		// Сохраняем загруженные данные
		$this->_modelColumns = $aLoadedValues;
		
		// Переключаем статус загрузки данных модели из БД
		$this->_loaded = TRUE;
		
		return $this;
	}
	
	/**
	 * Создает запрос к СУБД
	 */
	public function query()
	{
		$this->_oQuery = $this->_oDatabase->query();
		
		// По умолчанию добавляем в объект запроса имя таблицы, если она есть,
		// оператором по умолчанию устанавливаем SELECT
		if (!empty($this->_columnNames))
		{
			$this->_oQuery->table($this->_tableName)
				->select();
		}
		
		return $this->_oQuery;
	}
	
	/**
	 * Сохраняет данные, которые были переданы для этого в модель
	 */
	public function save()
	{
		// Если ничего для сохранения нет, ничего не делаем
		if (empty($this->_modelFieldsValues))
		{
			return NULL;
		}
		
		// Создаем объект запроса
		$oQuery_Save = $this->query();
		
		$oQuery_Save->table($this->_tableName);
			
		// Поля для вставки в таблицу
		$aFields = array();
		
		// Значения для вставки в таблицу
		$aValues = array();
		
		foreach ($this->_modelFieldsValues as $aFieldValue)
		{
			// Если поле не требовало внесения изменений, ничего не делаем
			if ($aFieldValue["changed"] === FALSE)
			{
				continue;
			}
			
			$aFields[] = $aFieldValue["field"];
			$aValues[] = $aFieldValue["value"];
		}
		
		// Добавляем имена полей и значения для сохранения
		$oQuery_Save->fields($aFields)
			->values($aValues);
		
		// Если у модели нет значения поля первичного ключа, данные нужно добавить в таблицу
		if (is_null($this->getPrimaryKey()))
		{
			$oQuery_Save->insert();
		}
		// Иначе нужно обновить данные в уже имеющейся строке
		else 
		{
			$oQuery_Save->update();
		}
		
		// Выполняем запрос
		$oQuery_Save->execute();
		
		// Если у модели не было значения первичного ключа, и если теперь такой первичный ключ есть
		if (is_null($this->getPrimaryKey()) && $oQuery_Save->lastInsertId())
		{
			// Устанавливаем значение первичного ключа для модели
			$this->setPrimaryKey($oQuery_Save->lastInsertId());
		}
		
		// Возвращаем вызову результирующий набор запроса
		return $oQuery_Save;
	}
	
	/**
	 * Устанавливает значение первичного ключа
	 */
	public function setPrimaryKey($iPrimaryKey = 0)
	{
		if ($iPrimaryKey)
		{
			$this->_primaryKey = $iPrimaryKey;
		}
		
		return $this;
	}
	
	/**
	 * Возвращает значение первичного ключа для объекта модели, если оно есть
	 */
	public function getPrimaryKey($primaryKeyFieldName = "id")
	{
		// Если значение было установлено ранее
		if (!is_null($this->_primaryKey))
		{
			// Вернем его
			return $this->_primaryKey;
		}
		
		// Если есть такое свойство
		if (!empty($this->$primaryKeyFieldName))
		{
			return $this->$primaryKeyFieldName;
		}
		
		// Если такого свойства нет, возможно, первичный ключ установлен не для поля ID или указанного вызовом поля
		$primaryKeyFieldName = $this->_columnNames["primary"];
		
		// Если есть такое свойство
		if (!empty($this->$primaryKeyFieldName))
		{
			return $this->$primaryKeyFieldName;
		}
		// Если нет и его
		else
		{
			return NULL;
		}
	}
	
	/**
	 * Устанавливает значения, которые необходимо сохранить или обновить в БД для модели
	 */
	protected function _setFieldValue($field, $value, $changed = TRUE)
	{
		$array = array(
			"field" => $field,
			"value" => $value,
			"changed" => $changed
		);
		
		$this->_modelFieldsValues[] = $array;
	}
	
	/**
	 * Чтобы обеспечить работу с моделями данных по типу 
	 * $Model->field($value) или $Model->field = $value
	 * 
	 * нам нужно определить так называемые магические методы __get(), __set() и __call()
	 */
	/**
	 * Получает значение указанного поля таблицы БД
	 * @param string $property
	 */
	public function __get($property)
	{
		$property = strtolower($property);
		
		$return = NULL;
		
		// Если запрошено поле id, попробуем вернуть первичный ключ
		if ($property === "id")
		{
			return $this->getPrimaryKey();
		}
		
		// Если указанное свойство есть в перечне полей таблицы, и оно не запрещено к чтению
		if (in_array($property, $this->_columnNames) && !in_array($property, $this->_forbiddenFields))
		{
			// Если данные не были загружены ранее
			if ($this->_loaded === FALSE)
			{
				// Загружаем данные
				$this->_load();
			}
			
			return ($this->_loaded === TRUE) ? $this->_modelColumns[$property] : NULL;
		}
		// Если такого свойства нет, возвращаем пустую строку
		else
		{
			return "";
		}
		
		return $return;
	}
	
	/**
	 * Вызов недоступных методов модели
	 */
	public function __call($func, $args)
	{
		// Если вызывается метод, название которого начинается с ->getByXXX
		if (preg_match("/^getBy(.+)$/Dsui", $func, $matches))
		{
			// Сохраняем значение для поиска
			$sValue = strval($args[0]);
			
			// Сохраняем имя поля таблицы, по которому происходит отбор
			$sFieldName = strtolower($matches[1]);
			
			// Выполняем запрос к БД
			$object = $this->query()
						// Отбираем поле по запрошенному значению
						->where($sFieldName, "=", $sValue)
						// Выполняем запрос
						->execute();
			// Получаем результат запроса
			$result = $object->fetchAll();
			
			// Если результат запроса не пустой, возвращаем его
			return (!empty($result)) ? $result[0] : NULL;
		}
		
		// Если не было передано никаких аргументов
		if (empty($args))
		{
			return NULL;
		}
		// Если устанавливается значение полей для сохранения или обновления
		$this->_setFieldValue($func, $args[0]);
		
		return $this;
	}
	
}
?>
Читатель, вероятно, обратил внимание еще и на метод save() класса Core_ORM. О том, зачем он и что делает, станет понятно позже.

Реализация метода query() представлена здесь же. Теперь давайте посмотрим, что происходит после того, как за методом query() вызываются еще методы ->where()->execute(). Для этого приведем код реализации класса Core_Database_Pdo.

<?php
// Файл modules/core/database/pdo.php

defined("LEZH") || exit("Доступ к файлу запрещен");

/**
 * Класс для создания и выполнения запросов к СУБД
 */
class Core_Database_Pdo
{
	// Подключение к СУБД
	protected $_pdo = NULL;
	
	// Формируемая строка запроса
	protected $_sql = "";
	
	// Значения оператора SELECT
	protected $_select = "";
	
	// Имя таблицы для запросов
	protected $_table = NULL;
	
	// Значения оператора FROM 
	protected $_from = "";
	
	// Условия оператора WHERE
	protected $_where = array();
	
	// Число затронутых запросом строк
	protected $_affectedRows = 0;
	
	// Идентификатор последней вставленной строки
	protected $_lastInsertId = NULL;
	
	// Значения оператора ORDER BY
	// По умолчанию сортировка делается по полю id по возрастанию
	protected $_orderBy = array(
		"id" => "ASC"
	);
	
	// Именя полей для операторов UPDATE или INSERT
	protected $_fields = array();
	
	// Вставляемые значения для операторов UPDATE или INSERT
	protected $_values = array();
	
	// Значения оператора INSERT
	protected $_insert = "";
	
	// Значения оператора UPDATE
	protected $_update = "";
	
	// Значения оператора DELETE 
	protected $_delete = "";
	
	// Объект подготовленного запроса к БД
	protected $_pdoStatement = NULL;
	
	// Результирующий запрос представить в виде анонимного объекта
	protected const FETCH_OBJ = 1;
	
	// Результирующий запрос представить в виде ассоциативного массива
	protected const FETCH_ASSOC = 2;
	
	// Тип представления результирующего запроса
	protected $_fetchType = NULL;
	
	public function __construct(PDO $object)
	{
		$this->_pdo = $object;
		
		// Устанавливаем тип представления результирующего запроса по умолчанию
		$this->_fetchType = static::FETCH_OBJ;
	}
	
	/**
	 * Получает перечень имен полен таблицы
	 */
	public function getColumnNames($tableName)
	{
		if (empty($tableName))
		{
			throw new Exception("Не передано имя таблицы");
		}
		
		// Формируем строку запроса
		$this->_sql .= "SHOW COLUMNS";
		
		$this->from($tableName);
		
		// Выполняем запрос
		// Сортировать нам будет нечего
		$this->clearOrderBy()
			->execute();
			
		$aFields = array();
		
		// Сохраняем поля таблицы в массив
		while ($row = $this->_pdoStatement->fetchObject())
		{	
			// Если поле имеет первичный ключ, отмечаем это особым образом
			if (!empty($row->Key) && $row->Key == "PRI")
			{
				$aFields["primary"] = $row->Field;
			}
			else 
			{
				$aFields[] = $row->Field;
			}
		}
		
		// Возвращаем вызову массив имен полей таблицы
		return $aFields;
	}
	
	/**
	 * Устанавливает имя таблицы, к которой выполняется запрос
	 */
	public function table($table)
	{
		if (empty($table))
		{
			throw new Exception("Не передано имя таблицы для запросов");
		}
		
		$this->_table = "`" . strval($table) . "`";
		
		return $this;
	}
	
	/**
	 * Получает все результаты запроса
	 */
	public function fetchAll()
	{
		// Для установленных типов представления результата запроса
		switch ($this->_fetchType)
		{
			// Для представления в виде ассоциативного массива
			case static::FETCH_ASSOC:
				
				$option = PDO::FETCH_ASSOC;
				
			break;
			
			// Для представления в виде анонимного объекта
			case static::FETCH_OBJ:
				
				$option = PDO::FETCH_OBJ;
				
			break;
		}
		
		// Получаем все результаты запроса
		$result = $this->_pdoStatement->fetchAll($option);
		
		$return = (!empty($result)) ? $result : NULL;
		
		// Возвращаем результаты вызову метода
		return $return;
	}
	
	/**
	 * Устанавливает тип представления результирующего запроса в виде ассоциативного массива
	 */
	public function asAssoc()
	{
		$this->_fetchType = static::FETCH_ASSOC;
		
		return $this;
	}
	
	/**
	 * Устанавливает тип представления результирующего запроса в виде анонимного объекта
	 */
	public function asObject()
	{
		$this->_fetchType = static::FETCH_OBJ;
		
		return $this;
	}
	
	public function select($selection = "*")
	{
		
		$this->_select = $selection;
		
		return $this;
	}
	
	public function from($tableName)
	{
		if (empty($tableName))
		{
			throw new Exception("Не передано имя таблицы для оператора FROM");
		}
		
		$tableName = htmlspecialchars(trim($tableName));
		
		$this->_from = "`$tableName`";
		
		return $this;
	}
	
	public function insert()
	{
		$this->_insert = "INSERT INTO ";
		
		return $this;
	}
	
	public function update()
	{
		$this->_update = "UPDATE ";
		
		return $this;
	}
	
	public function where($field, $operand, $condition)
	{
		$this->_where[] = array(
			"field" => strval($field),
			"operand" => strval($operand),
			"condition" => strval($condition),
			"or" => FALSE
		);
		
		return $this;
	}
	
	/**
	 * Выполняет запрос к СУБД
	 */
	public function execute()
	{
		// Выполняем запрос
		$sql = $this->build();
		
		$this->_pdoStatement = $this->_pdo->query($sql);
		
		// Сохраняем число затронутых строк
		if (!empty($this->_insert) || !empty($this->_update) || !empty($this->_delete))
		{
			$this->_affectedRows = $this->_pdoStatement->rowCount();
			
			// Если был вызван оператор INSERT 
			if (!empty($this->_insert))
			{
				// Сохраним идентификатор последней вставленной строки
				$this->_lastInsertId = $this->_pdo->lastInsertId();
			}
		}
		
		// Строку запроса сохраняем в массив
		$this->_lastQueryString($sql);
		
		// Очищаем саму строку запроса
		$this->clearSql();
		
		return $this;
	}
	
	/**
	 * Возвращает число затронутых запросом строк
	 */
	public function affectedRows()
	{
		return $this->_affectedRows;
	}
	
	/**
	 * Возвращает идентификатор последней вставленной строки
	 */
	public function lastInsertId()
	{
		return $this->_lastInsertId;
	}
	
	/**
	 * Сохраняет строку запроса в памяти
	 */
	protected function _lastQueryString(string $sql)
	{
		Core_Database::addSql($sql);
	}
	
	/**
	 * Очищает строку запроса
	 */
	public function clearSql()
	{
		$this->_sql = "";
		
		return $this;
	}
	
	/**
	 * Очищает параметры сортировки результатов запроса
	 */
	public function clearOrderBy()
	{
		$this->_orderBy = array();
		
		return $this;
	}
	
	/**
	 * Устанавливает перечень полей для операторов INSERT или UDPATE
	 */
	public function fields($data)
	{
		// Если данные переданы в виде массива
		if (is_array($data))
		{
			$this->_fields = $data;
		}
		// Если в виде строки
		elseif(is_string($data))
		{
			// Убираем пробелы и скобки из строки
			$data = preg_replace("/(\)|\(|\s)/", "", $data);
			$this->_fields = explode(",", $data);
		}
		else
		{
			throw new Exception("Не передано корректное значение массива полей таблицы для операторов INSERT или UPDATE");
		}
		
		return $this;
	}
	
	/**
	 * Устанавливает перечень значений для вставки операторами INSERT или UDPATE
	 */
	public function values($data)
	{
		// Если данные переданы в виде массива
		if (is_array($data))
		{
			$this->_values[] = $data;
		}
		// Если в виде строки
		elseif(is_string($data))
		{
			// Убираем пробелы и скобки из строки
			$data = preg_replace("/(\)|\(|\s)/", "", $data);
			$this->_values[] = explode(",", $data);
		}
		else
		{
			throw new Exception("Не передано корректное значение массива полей таблицы для операторов INSERT или UPDATE");
		}
		
		return $this;
	}
	
	/**
	 * Формирует строку SQL-запроса
	 */
	public function build()
	{
		// Получаем то значение строки, которое уже есть
		$sql = $this->_sql;
		
		// Если был объявлен оператор SELECT
		if (!empty($this->_select))
		{
			$sql = "SELECT " . $this->_select . " ";
		}
		
		// Если был объявлен оператор INSERT
		if (!empty($this->_insert))
		{
			$sql = $this->_insert;
		}
		
		// Если был объявлен оператор UPDATE
		if (!empty($this->_update))
		{
			$sql = $this->_update;
		}
		
		// Если был объявлен оператор FROM 
		if (!empty($this->_from))
		{
			$from = $this->_from;
		}
		elseif(!is_null($this->_table))
		{
			$from = $this->_table;
		}
		
		// Если не были объявлены INSERT или UPDATE
		$sql .= (empty($this->_insert) && empty($this->_update)) ?
				" FROM " . $from . " " :
				$from . " ";
		
		// Добавляем условия отбора результатов запроса
		if (!empty($this->_where) && empty($this->_insert))
		{
			// В любом случае выбираем только неудаленные элементы
			$sql .= " WHERE `deleted` = 0";
			
			// Перебираем все созданные условия отбора
			foreach ($this->_where as $aWhere)
			{
				// Если вместо AND требовалось использовать оператор OR, используем его
				$sql .= ($aWhere["or"] !== TRUE) ? " AND " : " OR ";
				
				// Формируем строку условия отбора
				$sql .= "`" . $aWhere["field"] . "` " . $aWhere["operand"] . " " . $this->_pdo->quote($aWhere["condition"]);
			}
		}
		
		// Если есть параметры сортировки результата запроса
		if (!empty($this->_orderBy) && empty($this->_insert) && empty($this->_update))
		{
			$sql .= " ORDER BY ";
			
			$i = 0;
			// Добавляем параметры сортировки
			foreach ($this->_orderBy as $orderByField => $orderByDirection)
			{
				// Если это не первый элемент сортировки, добавляем запятую
				$sql .= ($i > 0) ? ", " : "";
				
				// Формируем строку условия сортировки
				$sql .= "`" . $orderByField . "` " . $orderByDirection;
				
				// Увеличиваем счетчик условий сортировки
				++$i;
			}
		}
		
		// Если объявлен оператор INSERT
		if (!empty($this->_insert) && !empty($this->_fields) && !empty($this->_values))
		{
			$sql .= "(" . implode(",", $this->_fields) . ")";
			$sql .= " VALUES ";
			
			$i = 0;
			foreach ($this->_values as $aValues)
			{
				// Если это не первая группа значений, добавляем запятую
				$sql .= ($i > 0) ? ", " : "";
				
				$sql .= "(";
				
				$array = array();
				
				// Обрабатываем каждое из значений
				foreach ($aValues as $value)
				{
					$array[] = $this->_pdo->quote($value);
				}
				
				$sql .= implode(",", $array);
				
				$sql .= ")";
			}
		}
		
		
		// Сохраняем строку запроса
		$this->_sql = $sql;
		
		// Возвращаем строку запроса вызову метода
		return $this->_sql;
	}

}

Итак, если пользователя с таким логином нет, регистрация возможна. Переходим к описанию её реализации.

Сначала сохраняются переданные значения логина, пароля и подтвержденного пароля. А затем объект модели пользователя передается контроллеру. Давайте посмотрим на этот контроллер.

Контроллер объекта модели пользователя

<?php
// Файл modules/core/user/controller.php

defined("LEZH") || exit("Доступ к файлу запрещен");

class User_Controller extends Core_Controller
{
	
	// Логин и пароль пользователя
	protected $_userData = array();
	
	public function __construct(User_Model $oUser)
	{
		// Вызываем конструктор родительского класса,
		// который сохранит объект модели пользователя
		parent::__construct($oUser);
	}
	
	/**
	 * Проверяет пользовательские данные перед регистрацией
	 */
	public function checkRegistrationData()
	{
		if (empty($this->_userData["login"]))
		{
			exit("Не установлено значение логина пользователя для проверки");
		}
	
		if (empty($this->_userData["password"]))
		{
			exit("Не установлено значение пароля пользователя для проверки");
		}
	
		if (empty($this->_userData["password_approve"]))
		{
			exit("Не установлено значение подтверждения пароля пользователя для проверки");
		}
		
		// Введенные пароли должны быть идентичны
		if ($this->_userData["password"] !== $this->_userData["password_approve"])
		{
			return FALSE;
		}
		
		// В качестве логина пользователя должен быть указан корректный адрес электропочты
		if (!filter_var($this->_userData["login"], FILTER_VALIDATE_EMAIL))
		{
			return FALSE;
		}
		
		// Логин пользователя дожен быть уникальным
		$oUser = Core::factory("User")
			->getByLogin($this->_userData["login"]);
		
		// Если такой пользователь есть, прекращаем выполнение сценария
		if (!is_null($oUser))
		{
			return FALSE;
		}
		
		/**
		 * Нужно проверить пароль на соответствие нескольким требованиям
		 * 
		 * 1. Длина пароля не менее 6 и не более 32 символов
		 * 2. Пароль не может содержать символы кроме символов латинского алфавита и *-+!@#$[]{}'";:><,.
		 * 3. Пароль доджен содержать как минимум одну цифру, и как минимум одну заглавную букву
		 * 
		 * Для каждого из условий добавляем проверку
		 */
		if (!preg_match("/^.{6,32}$/", $this->_userData["password"]) ||
			preg_match("/[^-\w\d*+!@#$\[\]\{\}'\"

Сайт принадлежит ООО Группа Ралтэк. 2014 — 2021 гг