Каждый мажорный релиз PHP добавляет ряд новых возможностей, некоторые из которых действительно имеют значение. Для PHP 5.3 — это были пространства имен и анонимные функции. Для PHP 5.4 — трейты. Для PHP 5.5 — генераторы. Для 5.6 — списки аргументов переменной длины.

PHP 7 имеет большое количество новшеств и улучшений, делающих жизнь разработчика легче. Но я считаю, что самым важным и долгосрочным изменением является работа с типами. Совокупность новых фич изменит взгляд на PHP разработку в лучшую сторону.

Почему поддержка строгой типизации так важна? Она предоставляет программе — компилятору или рантайму и другим разработчикам ценную информацию о том, что вы пытались сделать, без необходимости исполнять код. Это дает три типа преимуществ:

  1. Становится намного легче сообщать другим разработчикам цель кода. Это практически как документация, только лучше!
  2. Строгая типизация дает коду узкую направленность поведения, что способствует повышению изоляции.
  3. Программа читает и понимает строгую типизацию точно также как человек, появляется возможность анализировать код и находить ошибки за вас… прежде чем вы его исполните!

В подавляющем большинстве случаев, явно типизированный код будет легче понять, он лучше структурирован и имеет меньше ошибок, чем слабо типизированный. Только в небольшом проценте случаев строгая типизация доставляет больше проблем, чем пользы, но не беспокойтесь, в 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)


  1. ZmeeeD
    21.09.2015 17:09
    +8

    > при наследовании тип не может быть изменен, даже нельзя сделать его более конкретным (подклассом, например)

    На самом деле это грустно, ибо для конкретных объектов от абстрактов, имело бы смысл сужать круг подозреваемых результатов… Но это лучше конечно чем ничего :)


    1. HaruAtari
      21.09.2015 19:10
      +3

      Вы не можете переопределить сигнатуру, но вы можете вернуть объект класса-наследника.


      1. ZmeeeD
        22.09.2015 15:45

        Это ясно, и ясно что по принципу Лисков это может и не нужно. И по сути возможность переопределять сигнатуру означало бы перегрузку функции, но тем не менее с тайп хинтингом это можно сейчас делать, а с возвращаемыми типами уже нет
        … вообщем то движение очень в сторону Java идут, и не факт что это хорошо для php именно для php, потому как если нет разницы то зачем выбирать в сторону php, когда экосистема java и синтаксис уже проверен годами!


        1. HaruAtari
          29.09.2015 11:27

          На сколько я знаю, сейчас в STRICT режиме интерпретатор ругается на изменение сигнатуры в наследнике. За исключение конструкторов.

          вообщем то движение очень в сторону Java идут, и не факт что это хорошо для php именно для php

          Вот тут я с вами полность согласен. Строгость это, конечно, хорошо. Но главное не переборщить, если нужен строгий язык, то, как вы сказали, надо взять Java и не париться. Каждой задаче — свой инструмент!


          1. Fesor
            29.09.2015 11:44
            +2

            если нужен строгий язык

            А если хочется что-то не такое строгое как Java и не такое фривольное как Ruby — то PHP самое то.


  1. vollossy
    21.09.2015 17:14
    +11

    <зануда>
    PHP все-таки по умолчанию является слабо-типизированным языком с динамической типизацией. Да, в 7-ой версии должен появиться strict-mode, но по умолчанию он выключен. А то, о чем Вы говорите, является все-таки type-hinting'ом.
    </зануда>

    Ну а вообще, отсутствие возможности вернуть null — довольно сомнительный плюс, теперь вместо обычной проверки на null, например при использовании ORM, нужно будет городить велосипед. Да и вообще создается впечатление, что php пытается напялить на себя костюм джавы, постоянно усложняясь, а за ним идет и весь php-мир, за некоторыми исключениями. И вот это по-моему не очень хорошо, ведь основным преимуществом php была простота разработки.


    1. haskel
      21.09.2015 17:27

      Вы по прежнему можете вернуть null, но тогда метод должен быть без типизации результата, а если с типизацией, то надо выкидывать исключение, делов-то.


      1. gaelpa
        21.09.2015 17:38
        +4

        Исключения для такого случая всё-таки сомнительная практика (по крайней мере в PHP).


        1. zim32
          29.09.2015 13:09
          -1

          Доктрина при попытке найти ентити по ключу кидает исключение. И это правильно

          Другое дело что java обязывает отлавливать все исключение, чего пхп пока делать не научился


          1. Fesor
            29.09.2015 13:51

            чего пхп пока делать не научился

            не PHP не научился а PHP-ники не научились. в Java это надо что бы не уранить все, в PHP всем пофигу.


            1. zim32
              29.09.2015 15:02

              Нет вы не поняли ) В Java на уровне языка запрещено неотлавливать исключения. Или объяви явно что ты пробрасываешь исключение дальше, или обработай его сразу в коде. От этого больше писанины, но насколько увереннее писать код в таком случае.


          1. VolCh
            30.09.2015 08:35
            +1

            Доктрина при попытке найти ентити по ключу кидает исключение. И это правильно


            /** @return object|null The entity instance or NULL if the entity can not be found. */
            


            И это правильно. Поиск по ключу операция семантически отличная от, например, получения значения в массиве по ключу. Для поиска результат «не найден» — не исключение.


            1. zim32
              30.09.2015 10:09
              -1

              Давно не смотрел. По ключу да, null возвращает. В DQL кидает.
              github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/AbstractQuery.php#L802


              1. Fesor
                30.09.2015 10:46
                +2

                Потому что это getSingleResult. Есть еще getOneOrNullResult


      1. vollossy
        21.09.2015 18:01

        Как уже выше отметили для некоторых случаев исключения — не лучший выход. А вот null все-таки очень универсальный тип, означающий «ничто».
        Давайте рассмотрим примеры, когда корректно выбрасывать исключение, а когда все-таки лучше null(примеры очень субъективны, буду рад конструктивной критике):

        • Корректнее вернуть null, например, при работе с ORM, например, какой-нибудь абстрактный Model::findByPk($id). При этом лучше, если у нас будет возможность указать хинтом, экземпляр какого класса должен быть возвращен методом findByPk
        • Доступ к элементу связанного списка по индексу(хотя кто же на php использует связанные списки!). В этом случае лучше выбросить исключение. При этом возвращаемые значения могут быть определенного типа либо же любого типа.


        1. 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, то не надо закладывать строгость.
          Мне интуитивно понятен именно такой подход, не понимаю полустрогости.


          1. gaelpa
            21.09.2015 21:40
            +1

            Ваш ник намекает, что вам ближе maybe-подход к обработке подобных случаев.
            Но в php это получится дороговато по ресурсам.

            >> если вы говорите точно, что этот метод должен вернуть SomeClass
            речь о том, что я хочу сказать, что этот метод должен вернуть SomeClass_или_null


            1. haskel
              21.09.2015 21:58
              +1

              Мой ник совсем не связан с моими пристрастиями :-)

              Я понял, вам надо это https://wiki.php.net/rfc/nullable_types
              Ну я не против, если в таком виде, но предпочитаю очевидную строгость.


          1. qRoC
            21.09.2015 21:43

            Так же как и при передаче параметра в функцию, если вы передадите null вместо SomeClass

            Мы передаём не SomeClass, а указатель на объект класса SomeClass. Так что null вполне уместен.


            1. haskel
              21.09.2015 21:51

              Ок, передайте в своем php null в функцию с type-hinting-ом, и не получите fatal error.


              1. qRoC
                21.09.2015 22:15
                +1

                Я про это и говорю, что null должен передаваться. Почему этого не сделали — не знаю.


                1. haskel
                  22.09.2015 01:22
                  +1

                  разные школы кунг-фу


          1. 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»


          1. VolCh
            23.09.2015 10:10
            +2

            Возвращение объекта определенного класса или null (вернее какого-то специального значения, будь то null, NullObject, false и т. п.) во многих случаях вполне нормальная ситуация, а не исключительная, например тот же поиск по уникальному ключу в репозитории — для репозитория это просто «не найден», а исключительная это ситуация (договор ссылается на несуществующего клиента, например) или нормальная (раз не найден, то надо создать нового) решается одним, как минимум, уровнем выше, клиентом репозитория. Но уж если возвращает не признак «не найден», то должен вернуть объект строго определенного класса. И это не полустрогость, а вполне нормальная строгость «или ..., или ...».


            1. Fesor
              24.09.2015 12:14

              К сожалению это все последствие зимней драмы с тайп хинтингом. Пока все холиварили RFC добавляющее нулабл как-то упустили


              1. VolCh
                25.09.2015 07:54
                +2

                Это понятно. Я к тому, что бросать исключения, если не можешь вернуть полноценный объект не потому, что не смог его создать, хотя от тебя это ожидалось, а потому, что не смог его найти в хранилище или ещё где — порочная практика в общем случае. И вернуть какое-то особое значение — нормально. А null это будет, особый инстанс обычного класса, обычный инстанс особого наследника обычного класса, обычный инстанс особого класса с тем же интерфейсом, что обычный класс — детали реализации, зачастую диктуемые языком.


    1. HaruAtari
      21.09.2015 19:12
      +5

      Запрет на null должен быть в языках с подержкой maybe типов и сопоставления с образцом. Там это очень лаконично вписывается. В php же это проблема, которая уже на рассмотрении, просто это не успели включить в версию. Ждем nullable значения в 7.1


  1. qRoC
    21.09.2015 19:09
    +1

    рантайму и другим разработчикам ценную информацию о том, что вы пытались сделать, без необходимости исполнять код.

    Рантайм обрабатывает как дополнительную проверку типа(type-hinting), это не полноценная типизация. Так что на оптимизации расчитывать не стоит.


    1. Fedot
      22.09.2015 17:42
      +1

      Тут вы не совсем правы. Один из разработчиков обещал после принятия указания скалярных типов, написать JIT для PHP, и во многом он будет основываться на type-hinting.
      Так что ждём что у него получится.


      1. qRoC
        23.09.2015 08:28

        Вы про ветку zend-jit?

        Разработчики phpng писали реализацию jit, и закинули. Так что не думаю что стоит сильно ждать поддержку JIT.


        1. Fedot
          23.09.2015 13:55

          Нет, я о другом. В ходе обсуждения rfc про type-hinting скалярных типов, один из разработчиков обещал после принятия этого rfc заняться написанием JIT.

          По поводу phpng, там ситуация была в том что они исследовали возможности улучшения производительности, и по началу пробовали JIT, но поняв что он не даёт сильного прироста производительности, после этого они копнули глубже и в итоге переписали очень много внутренностей. Что и позволило получить стоить значительный прирост. Но при этом идею с JIT они всё же не выкинули, а просто отложили. В своём докладе Дмитрий Стогов, в том числе говорил что на данный момент они без JIT очень близки к производительности HHVM, и он видит в этом место для роста. Т.е. вполне вероятно что в скором будущем JIT всё же появиться. И что учитывая изменения произошедшие в движке PHP уже может сулить так же не малый прирост производительности.

          Хотя нужно сказать что это ИМХО, так как прямых заявлений что кто-то _уже_ занимается разработкой JIT, я пока не видел.


          1. Fesor
            24.09.2015 12:22

            после принятия этого rfc заняться написанием JIT.

            Не JIT а полноценный компилятор, ну мол… информация о типах есть, так что предполагалось что PHP код написанный в стрикт режиме можно просто компилить напрямую в машинный код.

            прямых заявлений что кто-то _уже_ занимается разработкой JIT, я пока не видел

            А никто и не занимается. К этому скорее всего вернутся после стабилизации и релиза PHP7, глядишь в каком-нибудь PHP8 уже будет JIT.


            1. qRoC
              24.09.2015 14:25

              информация о типах есть, так что предполагалось что PHP код написанный в стрикт режиме можно просто компилить напрямую в машинный код.

              Ну информация о типах только для аргументов и возвращаемых значениях, не более… Да и то сомнительная. В поставке php идут функции которые в стрикт режиме просто не собрать(те противные функции которые возвращают результат или false в случае ошибки).


              1. Fesor
                24.09.2015 14:40

                Ну информация о типах только для аргументов и возвращаемых значениях, не более…

                Так сложно вычислить тип в момент компиляции? Почти все взрослые языки это умеют уже.

                В поставке php идут функции которые в стрикт режиме просто не собрать

                Они уже собраны, речь идет только о компиляции PHP в нативный код в обход виртуальной машины (грубо говоря экстеншены для PHP на PHP), с минимумом оверхэда связанного именно с вызовом вот этих ненадежных функций. Опять же, никто насколько я знаю еще ничего такого не пишет, а Антонио Ферера (не помню как его фамилия правильно произносится) только приводил идею подобной штуки как идею на будущее и хорошее подспорье для оптимизаций внутри JIT.


                1. qRoC
                  24.09.2015 14:59

                  Так сложно вычислить тип в момент компиляции? Почти все взрослые языки это умеют уже.

                  При использовании стандартных функций php это нереально.

                  грубо говоря экстеншены для PHP на PHP

                  В любом случае работаем с zval. И не забываем про __autoload. Так что идея довольно сомнительная, особенно если смотреть в сторону всяких phalcon1 в которых ключевая особенность «высокая производительность» на практике выглядит как маркетинговый ход.


                  1. Fesor
                    24.09.2015 17:52

                    В любом случае работаем с zval

                    А вот тут уже зависит от реализации. В прочем без типизированных массивов мечтать о анпакинге переменных не приходится… А компиляция штук вида:

                    function foo(int $a, int $b) : int {
                        return $a + $b;
                    }
                    


                    явного профита не даст.


  1. esudnik
    22.09.2015 01:27
    +4

    Надеюсь что в будущих версиях все еще можно будет использовать NULL. А то судя по всему PHP медленно но уверенно превращается в Java, где на одну строчку «кода» нужно добавлять 100 строк для различных типизаций и опработки ошибок.


    1. Fesor
      24.09.2015 12:23

      где на одну строчку «кода» нужно добавлять 100 строк для различных типизаций и опработки ошибок.

      Вас никто не заставляет использовать тайпхинтинг для возвращаемых значений или для скаляров. А вот мне эти плюшки будут полезны. Правда без nullable будет грустно пока, но думаю в PHP 7.1 и оно появится.