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) { 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}"); } }
Для примера реализуем класс 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»!
<?php $sep = DIRECTORY_SEPARATOR; $dir = dirname(__FILE__); // Подключаем трейт. include_once("{$dir}{$sep}traits{$sep}Validator.php"); // Подключаем класс, который будет использовать данный трейт. include_once("{$dir}{$sep}models{$sep}Pen.php"); 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; }
Заключение
Вот с помощью такого вот простенького трейта можно из слона сделать си-шарп валидировать входные/выходные параметры функций без копипастинга методов в разных классах.
Я специально не стал прикручивать к методу validate() в примере выше особые параметры валидации, например такие, как минимальное/максимальное значение double-ов или строковых переменных, пользовательские колбэки на валидацию параметров, вывод своего сообщения при выбросе исключения и так далее.
Потому что основной целью статьи было рассказать о том, что технология, позволяющая добиться полиморфизма, есть и доступна она даже на старой версии PHP.
ссылка на оригинал статьи https://habrahabr.ru/post/329360/
Добавить комментарий