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

от автора

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.

Весь исходник трейта

З.Ы. Я специально написал валидацию каждого примитива по отдельности, чтобы в дальнейшем была возможность передать в трейт массив со своими дополнительными правилами валидации. Например для 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)     {         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 (простая чернильная ручка, инициализирующаяся с каким-то количеством чернил), выводящий сообщение на экран.

Класс 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *