Контроль типа
PHP 5 предоставляет возможность использовать контроль типов. На данный момент функции имеют возможность заставлять параметры быть либо объектами (путем указания имени класса в прототипе функции), либо интерфейсами, либо массивами (начиная с PHP 5.1), или колбеком с типом callable (начиная с PHP 5.4). Однако, если NULL использовался как значение параметра по умолчанию, то это будет также допустимо в качестве аргумента для последующего вызова.
Если класс или интерфейс указан для контроля типа, то все его потомки или реализации также допустимы.
Контроль типа не может быть использован со скалярными типами, такими как int или string. Трейты также недопустимы.
Пример #1 Пример контроля типов
<?php
// Тестовый класс
class MyClass
{
/**
* Тестовая функция
*
* Первый параметр должен быть объектом типа OtherClass
*/
public function test(OtherClass $otherclass) {
echo $otherclass->var;
}
/**
* Другая тестовая функция
*
* Первый параметр должен быть массивом
*/
public function test_array(array $input_array) {
print_r($input_array);
}
/**
* Первый параметр должен быть итератором
*/
public function test_interface(Traversable $iterator) {
echo get_class($iterator);
}
/**
* Первый параметр должен быть типа callable
*/
public function test_callable(callable $callback, $data) {
call_user_func($callback, $data);
}
}
// Другой тестовый класс
class OtherClass {
public $var = 'Hello World';
}
?>
В случае передачи аргумента неправильного типа результатом будет фатальная ошибка.
<?php
// Экземпляры каждого класса
$myclass = new MyClass;
$otherclass = new OtherClass;
// Ошибка: Аргумент 1 должен быть экземпляром класса OtherClass
$myclass->test('hello');
// Ошибка: Аргумент 1 должен быть экземпляром класса OtherClass
$foo = new stdClass;
$myclass->test($foo);
// Ошибка: Аргумент 1 не должен быть null
$myclass->test(null);
// Работает: Выводит Hello World
$myclass->test($otherclass);
// Ошибка: Аргумент 1 должен быть массив
$myclass->test_array('a string');
// Работает: Выводит массив
$myclass->test_array(array('a', 'b', 'c'));
// Работает: Выводит ArrayObject
$myclass->test_interface(new ArrayObject(array()));
// Работает: Выводит int(1)
$myclass->test_callable('var_dump', 1);
?>
Также, контроль типов работает и с функциями:
<?php
// Пример класса
class MyClass {
public $var = 'Hello World';
}
/**
* Тестовая функция
*
* Первый параметр должен быть объект класса MyClass
*/
function myFunction(MyClass $foo) {
echo $foo->var;
}
// Это работает
$myclass = new MyClass;
myFunction($myclass);
?>
Контроль типов допускает значения NULL:
<?php
/* Прием значения NULL */
function test(stdClass $obj = NULL) {
}
test(NULL);
test(new stdClass);
?>
- Введение
- Основы
- Свойства
- Константы классов
- Автоматическая загрузка классов
- Конструкторы и деструкторы
- Область видимости
- Наследование
- Оператор разрешения области видимости (::)
- Ключевое слово "static"
- Абстрактные классы
- Интерфейсы объектов
- Трейты
- Anonymous classes
- Перегрузка
- Итераторы объектов
- Магические методы
- Ключевое слово "final"
- Клонирование объектов
- Сравнение объектов
- Контроль типа
- Позднее статическое связывание
- Объекты и ссылки
- Сериализация объектов
- Журнал изменений ООП
Коментарии
Type hinting works with interfaces too. In other words, you can specify the name of an interface for a function parameter, and the object passed in must implement that interface, or else type hinting throws an exception.
The manual's sample code says:
<?php
//...
// Fatal Error: Argument 1 must not be null
$myclass->test(null);
//...
?>
And this is true, unless a default value of NULL is given; in fact, this is the only way to give a default value for object arguments (as a default value must be a constant expression):
<?php
$mine = new MyClass();
$mine->test(NULL);
class MyClass{
public function __construct(OtherClass $arg = NULL){
if(is_null($arg)){
//Apply default value here.
}
}
public function test(array $arr = NULL){
print_r($arr);
}
}
class OtherClass{
}
?>
TYPE-HINTING and VISIBILITY
Type-hinting is just one more small piece of PHP that protects our objects when visibility cannot.
<?php
class Point {
public $x, $y;
public function __construct($xVal = 0, $yVal = 0) {
$this->x = $xVal;
$this->y = $yVal;
}
}
class Polyline {
protected $points = array();
public function addPoint(Point $p) { // the line we're interested in...
$this->points[] = $p;
}
}
$point1 = new Point(15, 12);
$polyline = new Polyline();
$polyline->addPoint($point1);
$polyline->addPoint(new Point(55, 22));
$polyline->addPoint(new Point(33, 31));
$polyline->addPoint(new stdClass()); // PHP will throw an error for us!
?>
Since our Polyline::addPoint() function has to be public, any outside code can try to pass anything. But, when type-hinting is declared, PHP throws an error when phoney data tries to sneak by.
One useful thing with Type Hinting that I could not find in the documentation (but tested) is that you can also use an Interface in the hint (versus a Class). This is a very useful tool if you are trying to code to Interfaces rather than Classes (which is common in Test Driven Development and Dependency Injection paradigms). It means your external class can present itself into the method as long as it implements the nominated Interface (obviously).
I have made a little bench between three method of type hinting for native type (string, integer, ...).
First method : by test type in function like :
<?php
function testTest($arg) {
if (!is_string($arg)) {
trigger_error('Argument $arg passed to test must be an instance of string, other given');
}
return $arg;
}
?>
Second method : by object representing native type :
<?php
function testObject(StringObj $arg) {
return $arg;
}
?>
Third method : by class TypeHint proposed by Daniel :
<?php
function testHint(string $arg) {
return $arg;
}
?>
the results are here :
bench for 100000 iterations, in seconds
avg min max total
test 5.3275489807129E-6 2.8610229492188E-6 0.0033020973205566 0.53275895118713
object 4.9089097976685E-6 3.814697265625E-6 0.0025870800018311 0.49089503288269
hint 3.2338891029358E-5 2.9802322387695E-5 0.0025920867919922 3.2338931560516
As you can see, the method by object is the best
now you know...
i use eclipse ganymede as an IDE and it offers "intellisense" where it can, i.e. when variables are "declared" via type hinting or a "new"-statement . i found using the following pattern helps eclipse along as well:
<?php
class MyClass{
public static function Cast(MyClass &$object=NULL){
return $object;
}
public method CallMe(){
}
}
$x=unserialize($someContent);
$x=MyObject::Cast($x);
$x->CallMe();
?>
after calling Cast(), due to the type hinting, eclipse offers me the "CallMe" function in a dropdown when i type "$x->" in the code afterwards.
i found this very practical und included the Cast() function in my code template for new classes. i've been wondering, if there is a drawback i oversaw, but i haven't noticed any negative effects so far... maybe some of you will find this just as handy as i do ;o)
p.s. do note: when used in inherited classes a STRICT-notice comes up, because the function definition of the inherited class doesn't match the parent's definition (different type hinting) - but it works great!
I've implemented a basic function to ensure argument's type.
<?php
/**
* Primary types
*/
class Type
{
const SKIP = 0;
const INT = 1;
const STRING = 2;
const BOOLEAN = 3;
const CALLBACK = 4;
const FLOAT = 5;
const RESOURCE = 6;
}
/**
* @throws InvalidArgumentException
*/
function ensureType()
{
$debugStack = debug_backtrace();
$argv = $debugStack[1]['args'];
$types = func_get_args();
foreach ($argv as $key => $value) {
$message = null;
if (is_null($value)) {
continue;
}
switch ($types[$key]) {
case Type::INT:
if (!is_int($value)) {
$message = 'Argument ' . $key . ' passed to ' . $debugStack[1]['function'] . '() must be of type int';
}
break;
case Type::STRING:
if (!is_string($value)) {
$message = 'Argument ' . $key . ' passed to ' . $debugStack[1]['function'] . '() must be of type string';
}
break;
case Type::BOOLEAN:
if (!is_bool($value)) {
$message = 'Argument ' . $key . ' passed to ' . $debugStack[1]['function'] . '() must be of type boolean';
}
break;
case Type::CALLBACK:
if (!is_callable($value)) {
$message = 'Argument ' . $key . ' passed to ' . $debugStack[1]['function'] . '() must be a valid callback';
}
break;
case Type::FLOAT:
if (!is_float($value)) {
$message = 'Argument ' . $key . ' passed to ' . $debugStack[1]['function'] . '() must be of type float';
}
break;
case Type::RESOURCE:
if (!is_resource($value)) {
$message = 'Argument ' . $key . ' passed to ' . $debugStack[1]['function'] . '() must be of type resource';
}
break;
}
if (!is_null($message)) {
if (is_object($value)) {
$message .= ', instance of ' . get_class($value) . ' given';
} else {
$message .= ', ' . gettype($value) . ' given';
}
throw new InvalidArgumentException($message);
}
}
}
function dummyFunction($var1, $var2, $var3)
{
ensureType(Type::BOOLEAN, Type::INT, Type::STRING);
}
$object = new ReflectionClass('ReflectionClass');
dummyFunction(1, $object, 'Hello there');
I've done some tests of the overhead that class Typehint gives us.
At my PC it goes as follows:
teststringNormal took: 0.041965961456299
teststringOverhead took: 0.48374915122986
It's like 10x longer time (not mention about memory usage), it's just because exception is thrown EVERY SINGLE TIME, along with expensive preg_match() and debug_backtrace() calls.
I think that using class in bigger applications will increase overhead like 100% or more.
<?php
function teststringOverhead(string $string) {
return $string;
}
function teststringNormal($string){
if(!is_string($string)){
return;
}
return $string;
}
$loopTimes = 20000;
/////////// test of overhead implementation vs normal
$t1 = microtime(true);
for($i = 0; $i <= $loopTimes; $i++) teststringNormal("xxx");
echo "<br>teststringNormal took: " . (microtime(true) - $t1);
$t2 = microtime(true);
for($i = 0; $i <= $loopTimes; $i++) teststringOverhead("xxx");
echo "<br>teststringOverhead took: " . (microtime(true) - $t2);
?>
i think this is pretty close.
$lines=hhb_mustbe('array',file("foofile"));
//either file succeeds, and returns an array, or file returns FALSE which is not an array, which throws an unexpectedValueException.
$socket=hhb_mustbe('Socket',socket_create(AF_INET,SOCK_STREAM,getprotobyname('tcp')));
//either socket_create succeeds, and returns a Socket, or socket_create returns False, which is not a resource of type Socket, and you get an UnexpectedValueException
$size=hhb_mustbe('int',filesize(somefile));
//either filesize() returns an integer, or returns FALSE wich is not an int, and you'll get UnexpectedValueException.
function hhb_mustbe(/*string*/$type,/*mixed*/$variable){
//should it be UnexpectedValueException or InvalidArgumentException?
//going with UnexpectedValueException for now...
$actual_type=gettype($variable);
if($actual_type==='unknown type'){
//i dont know how this can happen, but it is documented as a possible return value of gettype...
throw new Exception('could not determine the type of the variable!');
}
if($actual_type==='object'){
if(!is_a($variable,$type)){
$dbg=get_class($variable);
throw new UnexpectedValueException('variable is an object which does NOT implement class: '.$type.'. it is of class: '.$dbg);
}
return $variable;
}
if($actual_type==='resource'){
$dbg=get_resource_type($variable);
if($dbg!==$type){
throw new UnexpectedValueException('variable is a resource, which is NOT of type: '.$type.'. it is of type: '.$dbg);
}
return $variable;
}
//now a few special cases
if($type==='bool'){
$parsed_type='boolean';
} else if($type==='int'){
$parsed_type='integer';
} else if($type==='float'){
$parsed_type='double';
} else if($type==='null'){
$parsed_type='NULL';
} else{
$parsed_type=$type;
}
if($parsed_type!==$actual_type && $type!==$actual_type){
throw new UnexpectedValueException('variable is NOT of type: '.$type.'. it is of type: '.$actual_type);
}
//ok, variable passed all tests.
return $variable;
}
In regards to language.oop5.typehinting#103729 , when using PHP in Eclipse, the best way to typehint is already available in Eclipse natively.
The format is /* @var $variable ObjectName */
The single /* is important. You can also use namespaces, IE
/* @var $variable \Some\Namespace */
In short, there is no reason to create functions that return itself.
Please note, that with PHP 7, type hinting is faster than normal implementation.
This has to do with type hinting before PHP 7 allowing only objects and arrays and anything else (scalars) would throw an error.
I did some using code "doom at doom dot pl" posted 2 years ago
Here are results from couple of test runs using PHP 7 (200.000 loops)
- teststringNormal took: 0.01799488067627
- teststringOverhead took: 0.012195825576782
- teststringNormal took: 0.027030944824219
- teststringOverhead took: 0.012197017669678
- teststringNormal took: 0.017856121063232
- teststringOverhead took: 0.012274980545044
As you can see, the overhead is faster and much more consistent.
Here is one run with the is_string check removed from Normal:
- teststringNormal took: 0.010342836380005
- teststringOverhead took: 0.012849092483521
the following code demonstrate php7 response to various combination of inputs and type hinting in a simple matrix:
<?php
$array = [10, 10.12, true, '10USD', 'USD'];
function getValue($x, $y)
{
$typeMap = [
'integer' => 'int',
'int' => 'int',
'float' => 'float',
'double' => 'float',
'real' => 'float',
'string' => 'string',
'boolean' => 'bool',
'bool' => 'bool',
];
$yType = $typeMap[gettype($y)];
$call = 'test($x)';
if ($yType == 'string')
$call = 'test(\'$x\')';
$template = <<<TEMPLATE_FUNCTION
if (!function_exists('test')) {function test($yType \$arg){}}
try{
$call;
} catch (Error \$e) {
return \$e->getMessage();
}
return 'Pass';
TEMPLATE_FUNCTION;
return eval($template);
}
?>
<table border="1" width="50%">
<thead>
<tr><td></td><td align="center" colspan="<?= count($array) ?>"><strong>Input value</strong></td></tr>
<tr>
<td></td>
<?php foreach ($array as $header): ?>
<td><?= gettype($header), ' ', var_export($header) ?></td>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<tr></tr>
<?php foreach ($array as $y): ?>
<tr>
<td><?= gettype($y), ' ', var_export($y) ?></td>
<?php foreach ($array as $x): ?>
<td><?= getValue($x, $y); ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
This matrix demonstrate passing different variable types with different type hints:
<?php
$array = [10, 10.12, true, '10USD', 'USD'];
function getValue($x, $y)
{
$typeMap = [
'integer' => 'int',
'int' => 'int',
'float' => 'float',
'double' => 'float',
'real' => 'float',
'string' => 'string',
'boolean' => 'bool',
'bool' => 'bool',
];
$yType = $typeMap[gettype($y)];
$xType = $typeMap[gettype($x)];
if ($xType == 'string')
$x = "'$x'";
elseif ($xType == 'bool')
$x = $x ? 'true' : 'false';
try {
eval("if (!function_exists('test$yType')) {function test$yType($yType \$arg){}} test$yType($x);");
} catch (Error $e) {
return get_class($e).': '.$e->getMessage();
}
return 'Pass';
}
?>
<table border="1" width="50%">
<thead>
<tr>
<td></td>
<td align="center" colspan="<?= count($array) ?>"><strong>Input value</strong></td>
</tr>
<tr>
<td></td>
<?php foreach ($array as $header): ?>
<td><?= gettype($header), ' ', var_export($header) ?></td>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<tr></tr>
<?php foreach ($array as $y): ?>
<tr>
<td><?= gettype($y), ' ', var_export($y) ?></td>
<?php foreach ($array as $x): ?>
<td><?= getValue($x, $y); ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
Sometimes you want to work with classes (not their instances), but Typehinting is only permitted for an object (instance).
Here is sample code that tests whether the parameter is a valid type (the class/subclass/instance of an expected class):
<?php
declare(strict_types=1);
class MyClass {};
function myfunc( $class ) {
// Validate classname.
if ( $class instanceof MyClass ) {
$class = get_class( $class );
} elseif ( !class_exists( $class ) or !is_a( $class, MyClass::class, true ) ) {
throw new TypeError( "$class is not of type " . MyClass::class );
}
// Proceed with processing of MyClass subclass ...
}
$myclass = new MyClass;
myfunc( $myclass ); // valid
myfunc( MyClass::class ); // valid
myfunc( String::class ); // not of type MyClass.