📜 ⬆️ ⬇️

Type hinting according to all canons of polymorphism in old versions of PHP

tl; dr In short, in this article I will create a treit that allows even in versions of PHP less than 5.6 (up to version 5.4) to get from the compiler behavior similar to any static programming language. Moreover, the trait will validate not only the input parameters, but also the output parameters, too. So to speak, full immersion in hip-hinting.
This trait you can easily connect and use in your web applications.


Type hinting in PHP version over 7.0


PHP version <7 allows the definition of the method to describe what data types will be supplied to the function, and the output data type of the function.

Everything is wonderful here: what I asked, it came; what asked, it happened.
')
public function filterArray(array $arr, string $filterParameter, callable $filterCallback) : array 

We need to define our own rule for filtering the array — we took and created the lambda function, defined our own filter rule in it. And in filterArray () they passed $ arr , knowing in advance that this is an array, and not any integer.

If all of a sudden we pass not a string, but an object as $ filterParameter , PHP will instantly give us a parsing error. Like, we did not order this.

Type hinting in PHP version under 5.6


But PHP version <5.6 does not support explicit indication of output data types:

 public function sortArray($arr, $filterParam) : array // <-   { // ... } 

Also, PHP <5.6 does not support primitives as input data types, such as integer , string , float .

However, some types can even be specified in the old version of the language. For example, you can indicate that a parameter of type array , object , or an instance of a class will be passed to the function:

 /** * 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); } } 

Initial problem


But if you please. What should I do if I need to pass in the function not an array, but, for example, a double?

And a programmer can easily pass a string to a function, even an array, or an instance of any class.

The solution in this case is simple: you just have to independently check the input and output parameters for validity each time.

 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; } } 

However, it is scary to even think how many times you have to write the same code, which boils down to the following lines:

 if (null !== $param && '' !== $param) { return false; //  [],  '',          //  throw new Exception(__CLASS__ . __FUNCTION__ . ": Expected integer, got sting"); } 

The obvious solution to the problem is to write a validator in the trait, to which in the future to delegate all type checks of input parameters. In case the parameter is not of the type required, the parser will immediately throw an exception.

At the output we get the following:


Traits are essentially similar to protected methods in terms of being able to be called from any class in which it is imported. But, unlike inheritance, we can connect as many traits as necessary to a class and use all of its properties and methods.

Traits are available for use in PHP, starting with version 5.4.0.

All source trait
ZY I specifically wrote the validation of each primitive separately, so that in the future it would be possible to pass an array with my additional validation rules to the treit. For example, for integer, you can validate maxValue , minValue , isNatural , for strings, you can validate length instead of emptiness, and so on.
 <?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}"); } } 



Treit is used very simply. As an example, we implement the Notebook class, which stores in itself the methods of generating and obtaining a unique identifier in order to show how this function can be used to check the input and output data of a function.
 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; } } 


Another example: the Pen class (a simple ink pen initialized with some ink) that displays a message on the screen.

Pen class
 <?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; } } 


Well, now let's write our pen on the table: “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 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; } 


Conclusion:
 Pen writes message: hi habrahabr exception was throwed during validation of message 


Conclusion


With the help of such an unpretentious trait, it is possible to make C-Sharp from an elephant validate the input / output parameters of functions without copying and pasting methods in different classes.

I didn’t intentionally screw special validation parameters to the validate () method in the example above, such as the minimum / maximum value of doubles or string variables, custom callbacks for parameter validation, outputting my message when an exception was thrown, and so on.

Because the main purpose of the article was to tell that the technology that allows one to get static from the language is and is easy to implement even on the old version of PHP.

Source: https://habr.com/ru/post/329360/


All Articles