tl;dr Вкратце, в данной статье я создам трейт, позволящий даже в версиях PHP младше 5.6 (до версии 5.4) добиться от компилятора поведения, подобного любому статическому языку программирования. Причём трейт будет валидировать не только входные, но и выходные парамеры тоже. Так сказать, полное погружение в тайп-хинтинг.
Данный трейт вы сможете без проблем подключить и использовать в своих веб-приложениях.
Тайп-хинтинг в PHP версии старше 7.0
PHP версии < 7 позволяет в определении метода описать, какие типы данных будут поступать в функцию, и выходной тип данных функции.
Здесь всё замечательно: что задал, то и пришло; что задал, то и вышло.
public function filterArray(array $arr, string $filterParameter, callable $filterCallback) : array
Надо нам определить своё правило фильтрации массива – взяли и создали лямбда-функцию, определили в ней своё правило фильтрации. А в filterArray() передали $arr, заранее зная, что это массив, а не integer какой-нибудь.
Если вдруг в качестве $filterParameter передадим не string, а object, нам PHP мигом выдаст ошибку парсинга. Мол, мы сиё не заказывали.
Тайп-хинтинг в PHP версии младше 5.6
А вот PHP версии < 5.6 не поддерживает явное указание выходных типов данных:
public function sortArray($arr, $filterParam) : array // <- ошибка парсинга
{
// ...
}
Также PHP < 5.6 не поддерживает примитивы в качестве входных типов данных, такие как integer, string, float.
Однако некоторые типы можно указать даже на старой версии языка. Например, можно указать, что в функцию будет передан параметр типа array, object, либо экземпляр класса:
/**
* Class ArrayForSorting
* Будем предполагать, что это какая-то структура с кучей параметров, которые нам сейчас не важны.
*/
class ArrayForSorting
{
/**
* Массив для сортировки.
*
* @var array
*/
public $arrayForSorting;
/**
* @construct
*/
public function __construct($arrayForSorting)
{
$this->arrayForSorting = $arrayForSorting;
}
}
/**
* Class UserSortArray
* Класс, сортирующий массивы с помощью раздичных методов: вставки, слияния, пузырька.
*/
class UserSortArray
{
/**
* Доступные методы сортировки.
*
* @var object
*/
public $availableSortingMethods;
/**
* Сортировка методом вставки.
*
* @param ArrayForSorting $sortArray массив для сортировки, передаётся по ссылке.
*
* @throws UserSortArrayException если метод сортировки не доступен в системе.
*/
public function insertSort(ArrayForSorting &$sortArray)
{
if (false === isset($availableSortMethods->insertMethod)) {
throw new UserSortArrayException('Insert method for user array sort is not available.');
}
return uasort($sortArray->arrayForSorting, $availableSortMethods->bubbleMethod);
}
}
Исходная проблема
Но, извольте. Что делать, если мне потребуется в функцию передавать не array, а, к примеру, double?
И программист может запросто передать в функцию хоть строку, хоть массив, хоть экземпляр любого класса.
Выход в данном случае простой: нужно просто каждый раз самостоятельно проверять входные и выходные параметры на валидность.
class ArraySorter
{
public function sortArray(array &$sortArray, $userCallback)
{
// дабы не нарушать святы принципы полиморфизма,
// будем возвращать пустой массив в случае ошибки валидации,
// а не false или какой-нибудь -1.
if (false === $this->validateArray($sortArray)) {
return [];
}
return uasort($sortArray, $userCallback);
}
private function validateArray($array)
{
if (!isset($array) || false === is_array($array)) {
return false;
}
return true;
}
}
Однако страшно даже подумать, сколько раз придётся писать один и тот же код, сводящийся к следующим строчкам:
if (null !== $param && '' !== $param) {
return false; // или [], или '', или что ещё надо возвратить в случае невалидных параметров
// либо
throw new Exception(__CLASS__ . __FUNCTION__ . ": Expected integer, got sting");
}
Очевидное решение проблемы – написание валидатора в трейте, которому в дальнейшем делегировать все проверки типов входных параметров. В случае, если параметр имеет не тот тип, который требовался, парсер тут же бросит исключение.
На выходе мы получаем следующее:
- Язык становится менее динамически типизированным. Зато принципы ООП также не посылаются куда подальше программистом;
- Дублирующийся код проверок типов данных выносится в отдельную… сущность, если трейт так можно назвать;
- Новые валидаторы можно добавлять, не затрагивая структуру других классов.
Трейты по сути своей похожи на protected-методы в плане того, что их можно вызвать из любого класса, в который он импортирован. Но, в отличие от наследования, мы можем подключать сколько угодно трейтов в класс и использовать все его свойства и методы.
Трейты доступны для использования в PHP, начиная с версии 5.4.0.
<?php
namespace traits;
/**
* Trait Validator
* Трейт валидации параметров.
*/
trait Validator
{
/**
* Валидация параметров.
*
* @param array $validationParams массив правил валидации.
* Формат : 'тип' => значение.
* Если после типа идёт слово 'not_empty' -- идёт проверка параметра на пустоту
* (т.е. массив, не содержащий элементов, или пустая строка).
* В массиве содержатся следующие значения:
* [
* 'integer' => 123,
* 'string not_empty' => 'hello world!',
* 'array' => [ ... ],
* ]
*
* @return bool true если валидация прошла успешно.
*
* @throws Exception если метод валидации для типа данных не найден.
*/
public function validate($validationParams)
{
// Либо это массив, либо выбрасываем ошибку.
$this->validateArray($validationParams);
foreach ($validationParams as $type => $value) {
$methodName = 'validate' . ucfirst($type); // к примеру validateInteger
$isEmptinessValidation = false;
if ('not_empty' === substr($type, -9)) {
$methodName = 'validate' . ucfirst(substr($type, 0, -9));
$isEmptinessValidation = true;
}
if (false === method_exists($this, $methodName)) {
throw new Exception("Trait 'Validator' does not have method '{$methodName}'.");
}
// Либо возвращает true, либо выбрасывает исключение, одно из двух.
$this->{$methodName}($value, $isEmptinessValidation);
}
return true;
}
/**
* Валидирует строку.
*
* @param string $string валидируемая строка.
* @param bool $isValidateForEmptiness нужно ли валидировать строку на пустоту.
*
* @return bool результат валидации.
*/
public function validateString($string, $isValidateForEmptiness)
{
$validationRules = is_string($string) && $this->validateForSetAndEmptiness($string, $isValidateForEmptiness);
if (false === $validationRules) {
$this->throwError('string', gettype($string));
}
return true;
}
/**
* Валидирует булевую переменную.
*
* @param boolean $bool булевая переменная.
*
* @return bool результат валидации.
*/
public function validateBoolean($boolean, $isValidateForEmptiness = false)
{
$validationRules = isset($boolean) && is_bool($boolean);
if (false === $validationRules) {
$this->throwError('boolean', gettype($boolean));
}
return true;
}
/**
* Валидирует массив.
*
* @param string $array валидируемый массив.
* @param bool $isValidateForEmptiness нужно ли валидировать массив на пустоту.
*
* @return bool результат валидации.
*/
public function validateArray($array, $isValidateForEmptiness)
{
$validationRules = is_array($array) && $this->validateForSetAndEmptiness($array, $isValidateForEmptiness);
if (false === $validationRules) {
$this->throwError('array', gettype($array));
}
return true;
}
/**
* Валидирует объект.
*
* @param string $object валидируемый объект.
* @param bool $isValidateForEmptiness нужно ли валидировать объект на пустоту.
*
* @return bool результат валидации.
*/
public function validateObject($object, $isValidateForEmptiness)
{
$validationRules = is_object($object) && $this->validateForSetAndEmptiness($object, $isValidateForEmptiness);
if (false === $validationRules) {
$this->throwError('object', gettype($object));
}
return true;
}
/**
* Валидирует целое число.
*
* @param string $integer валидируемое число.
* @param bool $isValidateForEmptiness нужно ли валидировать число на пустоту.
*
* @return bool результат валидации.
*/
public function validateInteger($integer, $isValidateForEmptiness)
{
$validationRules = is_int($integer) && $this->validateForSetAndEmptiness($integer, false);
if (false === $validationRules) {
$this->throwError('integer', gettype($integer));
}
return true;
}
/**
* Валидирует параметр на установленность и на то, пустой ли параметр.
*
* @param string $parameter валидируемый параметр.
* @param bool $isValidateForEmptiness нужно ли валидировать параметр (объект, массив, строку) на пустоту.
*
* @return bool результат валидации.
*/
private function validateForSetAndEmptiness($parameter, $isValidateForEmptiness)
{
$isNotEmpty = true;
if (true === $isValidateForEmptiness) {
$isNotEmpty = false === empty($parameter);
}
return isset($parameter) && true === $isNotEmpty;
}
/**
* Бросает исключение.
*
* @param string $expectedType
* @param string $gotType
*
* @throws Exception в случае ошибки валидации входного параметра.
*/
private function throwError($expectedType, $gotType)
{
$validatorMethodName = ucfirst($expectedType) . 'Validator'; // integer -> IntegerValidator
throw new Exception("Parse error: {$validatorMethodName} expected type {$expectedType}, got {$gotType}");
}
}
Используется трейт очень просто. В качестве примера реализуем класс Notebook, хранящий в себе методы генерации и получения уникального идентификатора для того, чтобы показать, как можно с помощью данного трейта проверять входные и выходные данные функции.
namespace models;
use traits;
/**
* Class Notebook
* Ноутбук с уникальным идентификатором.
*/
class Notebook
{
use traitsValidator;
/**
* Уникальный ID ноутбука.
*
* @var string
*/
private $_uid;
/**
* @construct
*/
public function __construct()
{
$this->_uid = $this->generateUniqueIdentifier();
}
/**
* Возвращает уникальный ID ноутбука.
*
* @return string
*/
public function getNotebookUID()
{
// Метод validate() трейта принимает на вход массив с параметрами
// в стиле 'primitiveName' => $primitiveValue.
// При этом данный метод можно вызвать как в начале функции,
// так и в её конце.
$this->validate([
'string not_empty' => $this->_uid,
]);
return $this->_uid;
}
/**
* Генерирует уникальный ID ноутбука.
*
* @return string
*/
private function generateUniqueIdentifier()
{
$uniqueIdentifier = bin2hex(openssl_random_pseudo_bytes(40));
// А вот и пример валидации выходных параметров.
$this->validate([
'string not_empty' => $uniqueIdentifier,
]);
return $uniqueIdentifier;
}
}
Ещё один пример: класс Pen (простая чернильная ручка, инициализирующаяся с каким-то количеством чернил), выводящий сообщение на экран.
<?php
namespace models;
use traits;
/**
* Class Pen
* Обычная чернильная ручка.
*/
class Pen
{
use traitsValidator;
/**
* Оставшееся количество чернил ручки.
*
* @var double
*/
private $remainingAmountOfInk;
/**
* @construct
*/
public function __construct()
{
$this->remainingAmountOfInk = 100;
}
/**
* Выводит сообщение на экран.
*
* @param string $message сообщение.
*
* @return void
*
* @throws ValidatorException в случае ошибки валидации входных параметров.
*/
public function drawMessage($message)
{
$this->validate([
'string' => $message,
]);
if (0 > $this->remainingAmountOfInk) {
echo 'Ink ended'; // кончились чернила
}
echo 'Pen writes message: ' . $message . '<br>' . PHP_EOL;
$this->remainingAmountOfInk -= 1;
}
/**
* Возвращает оставшееся количество чернил.
*
* @return integer
*/
public function getRemainingAmountOfInk()
{
$this->validate([
'double' => $this->remainingAmountOfInk,
]);
return $this->remainingAmountOfInk;
}
}
Ну а теперь давайте распишем нашу ручку на столе: «Hello World»!
// autoload.php - класс автолоадера.
/**
* Class Autoloader
* Класс для автозагрузки классов и трейтов.
*/
class AutoLoader
{
/**
* Данный метод подгружает классы.
*
* @param string $className путь к классу.
*/
public static function loadClasses($className)
{
$dir = dirname(__FILE__);
$sep = DIRECTORY_SEPARATOR;
require_once("{$dir}{$sep}{$className}.php");
}
}
// Подгружаем классы.
spl_autoload_register([
'AutoLoader',
'loadClasses'
]);
// ------------------------------------
// index.php
include_once("autoload.php");
use models as m;
$pen = new mPen();
$pen->drawMessage('hi habrahabr'); // Pen writes message: hi habrahabr
$message = [
'message' => 'hi im message inside array',
];
try {
$pen->drawMessage($message); // будет выброшено исключение ValidatorException
} catch (Exception $e) {
echo 'exception was throwed during validation of message <br>' . PHP_EOL;
}
Вывод:
Pen writes message: hi habrahabr
exception was throwed during validation of message
Заключение
Вот с помощью такого вот простенького трейта можно из слона сделать си-шарп валидировать входные/выходные параметры функций без копипастинга методов в разных классах.
Я специально не стал прикручивать к методу validate() в примере выше особые параметры валидации, например такие, как минимальное/максимальное значение double-ов или строковых переменных, пользовательские колбэки на валидацию параметров, вывод своего сообщения при выбросе исключения и так далее.
Потому что основной целью статьи было рассказать о том, что технология, позволяющая добиться от языка статичности, есть и легко реализуема она даже на старой версии PHP.
Автор: the_kane_is_alive