PHP 7 имеет большое количество новшеств и улучшений, делающих жизнь разработчика легче. Но я считаю, что самым важным и долгосрочным изменением является работа с типами. Совокупность новых фич изменит взгляд на PHP разработку в лучшую сторону.
Почему поддержка строгой типизации так важна? Она предоставляет программе — компилятору или рантайму и другим разработчикам ценную информацию о том, что вы пытались сделать, без необходимости исполнять код. Это дает три типа преимуществ:
- Становится намного легче сообщать другим разработчикам цель кода. Это практически как документация, только лучше!
- Строгая типизация дает коду узкую направленность поведения, что способствует повышению изоляции.
- Программа читает и понимает строгую типизацию точно также как человек, появляется возможность анализировать код и находить ошибки за вас… прежде чем вы его исполните!
В подавляющем большинстве случаев, явно типизированный код будет легче понять, он лучше структурирован и имеет меньше ошибок, чем слабо типизированный. Только в небольшом проценте случаев строгая типизация доставляет больше проблем, чем пользы, но не беспокойтесь, в PHP проверка типов все еще является опциональной, также как и раньше. Но на самом деле, ее стоит использовать всегда.
Возвращаемые типы
Первым дополнением к уже существующей системе типизации будет поддержка возвращаемых типов. Теперь можно указывать в функциях и методах тип возвращаемого значения в явном виде. Рассмотрим следующий пример:
class Address {
protected $street;
protected $city;
protected $state;
protected $zip;
public function __construct($street, $city, $state, $zip) {
$this->street = $street;
$this->city = $city;
$this->state = $state;
$this->zip = $zip;
}
public function getStreet() { return $this->street; }
public function getCity() { return $this->city; }
public function getState() { return $this->state; }
public function getZip() { return $this->zip; }
}
class Employee {
protected $address;
public function __construct(Address $address) {
$this->address = $address;
}
public function getAddress() : Address {
return $this->address;
}
}
$a = new Address('123 Main St.', 'Chicago', 'IL', '60614');
$e = new Employee($a);
print $e->getAddress()->getStreet() . PHP_EOL;
// Prints 123 Main St.
В этом довольно приземленном примере у нас есть объект
Employee
, который имеет только одно свойство, содержащее переданный нами почтовый адрес. Обратите внимание на метод getAddress()
. После параметров функции у нас есть двоеточие и тип. Он является единственным типом, который может принимать возвращаемое значение.Постфиксный синтаксис для возвращаемых типов может показаться странным для разработчиков, привыкших к C/C++ или Java. Однако, на практике подход с префиксным объявлением не подходит для PHP, т.к. перед именем функции может идти множество ключевых слов. Во избежание проблем с парсером PHP выбрал путь схожий с Go, Rust и Scala.
При возврате любого другого типа методом
getAddress()
PHP будет выбрасывать исключение TypeError
. Даже null
не будет удовлетворять требованиям типа. Это позволяет нам с абсолютной уверенностью обращаться в print
к методом объекта Address
. Мы точно будем знать, что действительно вернется объект именно этого типа, не null
, не false, не строка или какой-то другой объект. Именно этим обеспечивается безопасность работы и отсутствие необходимости в дополнительных проверках, что в свою очередь делает наш собственный код чище. Даже если что-то пойдет не так, PHP обязательно предупредит нас.Но что делать, если у нас менее тривиальный случай и необходимо обрабатывать ситуации, когда нет объекта
Address
? Введем EmployeeRepository
, логика которого позволяет не иметь записей. Сначала мы добавим классу Employee
поле ID:class Employee {
protected $id;
protected $address;
public function __construct($id, Address $address) {
$this->id = $id;
$this->address = $address;
}
public function getAddress() : Address {
return $this->address;
}
}
А теперь создадим наш репозиторий. (В качестве заглушки добавим фиктивные данные прямо в конструктор, но на практике, конечно же, необходимо иметь источник данных для нашего репозитория).
class EmployeeRepository {
private $data = [];
public function __construct() {
$this->data[123] = new Employee(123, new Address('123 Main St.', 'Chicago', 'IL', '60614'));
$this->data[456] = new Employee(456, new Address('45 Hull St', 'Boston', 'MA', '02113'));
}
public function findById($id) : Employee {
return $this->data[$id];
}
}
$r = new EmployeeRepository();
print $r->findById(123)->getAddress()->getStreet() . PHP_EOL;
Большинство читателей быстро заметит, что `findById()` имеет баг, т.к. в случае, если мы попросим несуществующий идентификатор сотрудника PHP будет возвращать `null` и наш вызов `getAddress()` умрет с ошибкой «method called on non-object». Но на самом деле ошибка не там. Она заключается в том, что `findById()` должен возвращать сотрудника. Мы указываем возвращаемый тип `Employee`, чтобы было ясно чья это ошибка.
Что же делать, если действительно нет такого сотрудника? Есть два варианта: первый — исключение; если мы не можем вернуть то, что мы обещаем — это повод для особой обработки за пределами нормального течения кода. Другой — указание интерфейса, имплементация которого и будет возвращена (в том числе и «пустая»). Таким образом, оставшаяся часть кода будет работать и мы сможем контролировать происходящее в «пустых» случаях.
Выбор подхода зависит от варианта использования и также определяется рамками недопустимости последствий в случае невозвращения правильного типа. В случае репозитория, я бы поспорил с выбором в пользу исключений, поскольку шансы попасть именно в такую ситуацию минимальны, а работа через исключения является довольно дорогостоящей по производительности. Если бы мы имели дело со скалярной переменной, то обработка «пустого значения» было бы приемлемым выбором. Модифицируем наш код соответственно (для краткости показаны только измененные части):
interface AddressInterface {
public function getStreet();
public function getCity();
public function getState();
public function getZip();
}
class EmptyAddress implements AddressInterface {
public function getStreet() { return ''; }
public function getCity() { return ''; }
public function getState() { return ''; }
public function getZip() { return ''; }
}
class Address implements AddressInterface {
// ...
}
class Employee {
// ...
public function getAddress() : AddressInterface {
return $this->address;
}
}
class EmployeeRepository {
// ...
public function findById($id) : Employee {
if (!isset($this->data[$id])) {
throw new InvalidArgumentException('No such Employee: ' . $id);
}
return $this->data[$id];
}
}
try {
print $r->findById(123)->getAddress()->getStreet() . PHP_EOL;
print $r->findById(789)->getAddress()->getStreet() . PHP_EOL;
} catch (InvalidArgumentException $e) {
print $e->getMessage() . PHP_EOL;
}
/*
* Prints:
* 123 Main St.
* No such Employee: 789
*/
Теперь
getStreet()
будет отдавать хорошее пустое значение.Одно важное примечание о возвращаемых типах: при наследовании тип не может быть изменен, даже нельзя сделать его более конкретным (подклассом, например). Причина в особенностях ленивой загрузки PHP.
Возвращаемые типы являются большой, но далеко не единственной новой особенностью, расширяющей систему типов PHP. Во второй части мы рассмотрим другое, пожалуй даже более важное изменение: декларирование скалярных типов.
Комментарии (38)
vollossy
21.09.2015 17:14+11<зануда>
PHP все-таки по умолчанию является слабо-типизированным языком с динамической типизацией. Да, в 7-ой версии должен появиться strict-mode, но по умолчанию он выключен. А то, о чем Вы говорите, является все-таки type-hinting'ом.
</зануда>
Ну а вообще, отсутствие возможности вернуть null — довольно сомнительный плюс, теперь вместо обычной проверки на null, например при использовании ORM, нужно будет городить велосипед. Да и вообще создается впечатление, что php пытается напялить на себя костюм джавы, постоянно усложняясь, а за ним идет и весь php-мир, за некоторыми исключениями. И вот это по-моему не очень хорошо, ведь основным преимуществом php была простота разработки.haskel
21.09.2015 17:27Вы по прежнему можете вернуть null, но тогда метод должен быть без типизации результата, а если с типизацией, то надо выкидывать исключение, делов-то.
gaelpa
21.09.2015 17:38+4Исключения для такого случая всё-таки сомнительная практика (по крайней мере в PHP).
zim32
29.09.2015 13:09-1Доктрина при попытке найти ентити по ключу кидает исключение. И это правильно
Другое дело что java обязывает отлавливать все исключение, чего пхп пока делать не научилсяFesor
29.09.2015 13:51чего пхп пока делать не научился
не PHP не научился а PHP-ники не научились. в Java это надо что бы не уранить все, в PHP всем пофигу.zim32
29.09.2015 15:02Нет вы не поняли ) В Java на уровне языка запрещено неотлавливать исключения. Или объяви явно что ты пробрасываешь исключение дальше, или обработай его сразу в коде. От этого больше писанины, но насколько увереннее писать код в таком случае.
VolCh
30.09.2015 08:35+1Доктрина при попытке найти ентити по ключу кидает исключение. И это правильно
/** @return object|null The entity instance or NULL if the entity can not be found. */
И это правильно. Поиск по ключу операция семантически отличная от, например, получения значения в массиве по ключу. Для поиска результат «не найден» — не исключение.zim32
30.09.2015 10:09-1Давно не смотрел. По ключу да, null возвращает. В DQL кидает.
github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/AbstractQuery.php#L802
vollossy
21.09.2015 18:01Как уже выше отметили для некоторых случаев исключения — не лучший выход. А вот null все-таки очень универсальный тип, означающий «ничто».
Давайте рассмотрим примеры, когда корректно выбрасывать исключение, а когда все-таки лучше null(примеры очень субъективны, буду рад конструктивной критике):
- Корректнее вернуть null, например, при работе с ORM, например, какой-нибудь абстрактный Model::findByPk($id). При этом лучше, если у нас будет возможность указать хинтом, экземпляр какого класса должен быть возвращен методом findByPk
- Доступ к элементу связанного списка по индексу(хотя кто же на php использует связанные списки!). В этом случае лучше выбросить исключение. При этом возвращаемые значения могут быть определенного типа либо же любого типа.
haskel
21.09.2015 21:35+1Не соглашусь: если вы говорите точно, что этот метод должен вернуть SomeClass, то он уже не просто МОЖЕТ, а ДОЛЖЕН вернуть его. И если у метода нет такой возможности, то это исключительная ситуация. Так же как и при передаче параметра в функцию, если вы передадите null вместо SomeClass, то получите «Argument 1 passed to some_func() must be an instance of SomeClass, null given».
Если вы добавляете строгости своему коду, то нужно ее соблюдать, а если хотите null, то не надо закладывать строгость.
Мне интуитивно понятен именно такой подход, не понимаю полустрогости.gaelpa
21.09.2015 21:40+1Ваш ник намекает, что вам ближе maybe-подход к обработке подобных случаев.
Но в php это получится дороговато по ресурсам.
>> если вы говорите точно, что этот метод должен вернуть SomeClass
речь о том, что я хочу сказать, что этот метод должен вернуть SomeClass_или_nullhaskel
21.09.2015 21:58+1Мой ник совсем не связан с моими пристрастиями :-)
Я понял, вам надо это https://wiki.php.net/rfc/nullable_types
Ну я не против, если в таком виде, но предпочитаю очевидную строгость.
qRoC
21.09.2015 21:43Так же как и при передаче параметра в функцию, если вы передадите null вместо SomeClass
Мы передаём не SomeClass, а указатель на объект класса SomeClass. Так что null вполне уместен.
levchick
22.09.2015 12:27+2Передавать в метод null при налачии типизированно параметра можно, при условии если этот параметр имеет значение по умолчанию null
Например
function doSomething(SomeClass $arg=null){}
не вызовет ошибки при передачи null
однако
function doSomething(SomeClass $arg){}
действительно даст «Argument 1 passed to doSomething must be an instance of SomeClass, null given»
VolCh
23.09.2015 10:10+2Возвращение объекта определенного класса или null (вернее какого-то специального значения, будь то null, NullObject, false и т. п.) во многих случаях вполне нормальная ситуация, а не исключительная, например тот же поиск по уникальному ключу в репозитории — для репозитория это просто «не найден», а исключительная это ситуация (договор ссылается на несуществующего клиента, например) или нормальная (раз не найден, то надо создать нового) решается одним, как минимум, уровнем выше, клиентом репозитория. Но уж если возвращает не признак «не найден», то должен вернуть объект строго определенного класса. И это не полустрогость, а вполне нормальная строгость «или ..., или ...».
Fesor
24.09.2015 12:14К сожалению это все последствие зимней драмы с тайп хинтингом. Пока все холиварили RFC добавляющее нулабл как-то упустили
VolCh
25.09.2015 07:54+2Это понятно. Я к тому, что бросать исключения, если не можешь вернуть полноценный объект не потому, что не смог его создать, хотя от тебя это ожидалось, а потому, что не смог его найти в хранилище или ещё где — порочная практика в общем случае. И вернуть какое-то особое значение — нормально. А null это будет, особый инстанс обычного класса, обычный инстанс особого наследника обычного класса, обычный инстанс особого класса с тем же интерфейсом, что обычный класс — детали реализации, зачастую диктуемые языком.
HaruAtari
21.09.2015 19:12+5Запрет на null должен быть в языках с подержкой maybe типов и сопоставления с образцом. Там это очень лаконично вписывается. В php же это проблема, которая уже на рассмотрении, просто это не успели включить в версию. Ждем nullable значения в 7.1
qRoC
21.09.2015 19:09+1рантайму и другим разработчикам ценную информацию о том, что вы пытались сделать, без необходимости исполнять код.
Рантайм обрабатывает как дополнительную проверку типа(type-hinting), это не полноценная типизация. Так что на оптимизации расчитывать не стоит.Fedot
22.09.2015 17:42+1Тут вы не совсем правы. Один из разработчиков обещал после принятия указания скалярных типов, написать JIT для PHP, и во многом он будет основываться на type-hinting.
Так что ждём что у него получится.qRoC
23.09.2015 08:28Вы про ветку zend-jit?
Разработчики phpng писали реализацию jit, и закинули. Так что не думаю что стоит сильно ждать поддержку JIT.Fedot
23.09.2015 13:55Нет, я о другом. В ходе обсуждения rfc про type-hinting скалярных типов, один из разработчиков обещал после принятия этого rfc заняться написанием JIT.
По поводу phpng, там ситуация была в том что они исследовали возможности улучшения производительности, и по началу пробовали JIT, но поняв что он не даёт сильного прироста производительности, после этого они копнули глубже и в итоге переписали очень много внутренностей. Что и позволило получить стоить значительный прирост. Но при этом идею с JIT они всё же не выкинули, а просто отложили. В своём докладе Дмитрий Стогов, в том числе говорил что на данный момент они без JIT очень близки к производительности HHVM, и он видит в этом место для роста. Т.е. вполне вероятно что в скором будущем JIT всё же появиться. И что учитывая изменения произошедшие в движке PHP уже может сулить так же не малый прирост производительности.
Хотя нужно сказать что это ИМХО, так как прямых заявлений что кто-то _уже_ занимается разработкой JIT, я пока не видел.Fesor
24.09.2015 12:22после принятия этого rfc заняться написанием JIT.
Не JIT а полноценный компилятор, ну мол… информация о типах есть, так что предполагалось что PHP код написанный в стрикт режиме можно просто компилить напрямую в машинный код.
прямых заявлений что кто-то _уже_ занимается разработкой JIT, я пока не видел
А никто и не занимается. К этому скорее всего вернутся после стабилизации и релиза PHP7, глядишь в каком-нибудь PHP8 уже будет JIT.qRoC
24.09.2015 14:25информация о типах есть, так что предполагалось что PHP код написанный в стрикт режиме можно просто компилить напрямую в машинный код.
Ну информация о типах только для аргументов и возвращаемых значениях, не более… Да и то сомнительная. В поставке php идут функции которые в стрикт режиме просто не собрать(те противные функции которые возвращают результат или false в случае ошибки).Fesor
24.09.2015 14:40Ну информация о типах только для аргументов и возвращаемых значениях, не более…
Так сложно вычислить тип в момент компиляции? Почти все взрослые языки это умеют уже.
В поставке php идут функции которые в стрикт режиме просто не собрать
Они уже собраны, речь идет только о компиляции PHP в нативный код в обход виртуальной машины (грубо говоря экстеншены для PHP на PHP), с минимумом оверхэда связанного именно с вызовом вот этих ненадежных функций. Опять же, никто насколько я знаю еще ничего такого не пишет, а Антонио Ферера (не помню как его фамилия правильно произносится) только приводил идею подобной штуки как идею на будущее и хорошее подспорье для оптимизаций внутри JIT.qRoC
24.09.2015 14:59Так сложно вычислить тип в момент компиляции? Почти все взрослые языки это умеют уже.
При использовании стандартных функций php это нереально.
грубо говоря экстеншены для PHP на PHP
В любом случае работаем с zval. И не забываем про __autoload. Так что идея довольно сомнительная, особенно если смотреть в сторону всяких phalcon1 в которых ключевая особенность «высокая производительность» на практике выглядит как маркетинговый ход.Fesor
24.09.2015 17:52В любом случае работаем с zval
А вот тут уже зависит от реализации. В прочем без типизированных массивов мечтать о анпакинге переменных не приходится… А компиляция штук вида:
function foo(int $a, int $b) : int { return $a + $b; }
явного профита не даст.
esudnik
22.09.2015 01:27+4Надеюсь что в будущих версиях все еще можно будет использовать NULL. А то судя по всему PHP медленно но уверенно превращается в Java, где на одну строчку «кода» нужно добавлять 100 строк для различных типизаций и опработки ошибок.
Fesor
24.09.2015 12:23где на одну строчку «кода» нужно добавлять 100 строк для различных типизаций и опработки ошибок.
Вас никто не заставляет использовать тайпхинтинг для возвращаемых значений или для скаляров. А вот мне эти плюшки будут полезны. Правда без nullable будет грустно пока, но думаю в PHP 7.1 и оно появится.
ZmeeeD
> при наследовании тип не может быть изменен, даже нельзя сделать его более конкретным (подклассом, например)
На самом деле это грустно, ибо для конкретных объектов от абстрактов, имело бы смысл сужать круг подозреваемых результатов… Но это лучше конечно чем ничего :)
HaruAtari
Вы не можете переопределить сигнатуру, но вы можете вернуть объект класса-наследника.
ZmeeeD
Это ясно, и ясно что по принципу Лисков это может и не нужно. И по сути возможность переопределять сигнатуру означало бы перегрузку функции, но тем не менее с тайп хинтингом это можно сейчас делать, а с возвращаемыми типами уже нет
… вообщем то движение очень в сторону Java идут, и не факт что это хорошо для php именно для php, потому как если нет разницы то зачем выбирать в сторону php, когда экосистема java и синтаксис уже проверен годами!
HaruAtari
На сколько я знаю, сейчас в STRICT режиме интерпретатор ругается на изменение сигнатуры в наследнике. За исключение конструкторов.
Вот тут я с вами полность согласен. Строгость это, конечно, хорошо. Но главное не переборщить, если нужен строгий язык, то, как вы сказали, надо взять Java и не париться. Каждой задаче — свой инструмент!
Fesor
А если хочется что-то не такое строгое как Java и не такое фривольное как Ruby — то PHP самое то.