tl;dr Вкратце, в данной статье я создам трейт, позволяющий даже в версиях PHP младше 5.6 (до версии 5.4) добиться от компилятора поведения, подобного любому статическому языку программирования. Причём трейт будет валидировать не только входные, но и выходные парамеры тоже. Так сказать, полное погружение в тайп-хинтинг.
Данный трейт вы сможете без проблем подключить и использовать в своих веб-приложениях.
PHP версии < 7 позволяет в определении метода описать, какие типы данных будут поступать в функцию, и выходной тип данных функции.
Здесь всё замечательно: что задал, то и пришло; что задал, то и вышло.
Надо нам определить своё правило фильтрации массива – взяли и создали лямбда-функцию, определили в ней своё правило фильтрации. А в filterArray() передали $arr, заранее зная, что это массив, а не integer какой-нибудь.
Если вдруг в качестве $filterParameter передадим не string, а object, нам PHP мигом выдаст ошибку парсинга. Мол, мы сиё не заказывали.
А вот PHP версии < 5.6 не поддерживает явное указание выходных типов данных:
Также PHP < 5.6 не поддерживает примитивы в качестве входных типов данных, такие как integer, string, float.
Однако некоторые типы можно указать даже на старой версии языка. Например, можно указать, что в функцию будет передан параметр типа array, object, либо экземпляр класса:
Но, извольте. Что делать, если мне потребуется в функцию передавать не array, а, к примеру, double?
И программист может запросто передать в функцию хоть строку, хоть массив, хоть экземпляр любого класса.
Выход в данном случае простой: нужно просто каждый раз самостоятельно проверять входные и выходные параметры на валидность.
Однако страшно даже подумать, сколько раз придётся писать один и тот же код, сводящийся к следующим строчкам:
Очевидное решение проблемы – написание валидатора в трейте, которому в дальнейшем делегировать все проверки типов входных параметров. В случае, если параметр имеет не тот тип, который требовался, парсер тут же бросит исключение.
На выходе мы получаем следующее:
Трейты по сути своей похожи на protected-методы в плане того, что их можно вызвать из любого класса, в который он импортирован. Но, в отличие от наследования, мы можем подключать сколько угодно трейтов в класс и использовать все его свойства и методы.
Трейты доступны для использования в PHP, начиная с версии 5.4.0.
Используется трейт очень просто. В качестве примера реализуем класс Notebook, хранящий в себе методы генерации и получения уникального идентификатора для того, чтобы показать, как можно с помощью данного трейта проверять входные и выходные данные функции.
Ещё один пример: класс Pen (простая чернильная ручка, инициализирующаяся с каким-то количеством чернил), выводящий сообщение на экран.
Ну а теперь давайте распишем нашу ручку на столе: «Hello World»!
Вывод:
Вот с помощью такого вот простенького трейта можноиз слона сделать си-шарп валидировать входные/выходные параметры функций без копипастинга методов в разных классах.
Я специально не стал прикручивать к методу validate() в примере выше особые параметры валидации, например такие, как минимальное/максимальное значение double-ов или строковых переменных, пользовательские колбэки на валидацию параметров, вывод своего сообщения при выбросе исключения и так далее.
Потому что основной целью статьи было рассказать о том, что технология, позволяющая добиться от языка статичности, есть и легко реализуема она даже на старой версии PHP.
Данный трейт вы сможете без проблем подключить и использовать в своих веб-приложениях.
Тайп-хинтинг в 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.
Весь исходник трейта
З.Ы. Я специально написал валидацию каждого примитива по отдельности, чтобы в дальнейшем была возможность передать в трейт массив со своими дополнительными правилами валидации. Например для integer-а можно провалидировать maxValue, minValue, isNatural, для строк можно валидировать length вместо emptiness и так далее.
<?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 \traits\Validator; /** * Уникальный 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 (простая чернильная ручка, инициализирующаяся с каким-то количеством чернил), выводящий сообщение на экран.
Класс Pen
<?php namespace models; use traits; /** * Class Pen * Обычная чернильная ручка. */ class Pen { use \traits\Validator; /** * Оставшееся количество чернил ручки. * * @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»!
use models as m; $pen = new m\Pen(); $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.
