PHP Разработчик
24,0
рейтинг
21 сентября 2015 в 15:53

Разработка → Новый PHP, часть 1: Return types перевод tutorial

Каждый мажорный релиз 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. Во второй части мы рассмотрим другое, пожалуй даже более важное изменение: декларирование скалярных типов.
Перевод: Larry Garfield
Илья Гусев @iGusev
карма
45,5
рейтинг 24,0
PHP Разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (38)

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

    На самом деле это грустно, ибо для конкретных объектов от абстрактов, имело бы смысл сужать круг подозреваемых результатов… Но это лучше конечно чем ничего :)
    • +3
      Вы не можете переопределить сигнатуру, но вы можете вернуть объект класса-наследника.
      • 0
        Это ясно, и ясно что по принципу Лисков это может и не нужно. И по сути возможность переопределять сигнатуру означало бы перегрузку функции, но тем не менее с тайп хинтингом это можно сейчас делать, а с возвращаемыми типами уже нет
        … вообщем то движение очень в сторону Java идут, и не факт что это хорошо для php именно для php, потому как если нет разницы то зачем выбирать в сторону php, когда экосистема java и синтаксис уже проверен годами!
        • 0
          На сколько я знаю, сейчас в STRICT режиме интерпретатор ругается на изменение сигнатуры в наследнике. За исключение конструкторов.

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

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

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

    Ну а вообще, отсутствие возможности вернуть null — довольно сомнительный плюс, теперь вместо обычной проверки на null, например при использовании ORM, нужно будет городить велосипед. Да и вообще создается впечатление, что php пытается напялить на себя костюм джавы, постоянно усложняясь, а за ним идет и весь php-мир, за некоторыми исключениями. И вот это по-моему не очень хорошо, ведь основным преимуществом php была простота разработки.
    • 0
      Вы по прежнему можете вернуть null, но тогда метод должен быть без типизации результата, а если с типизацией, то надо выкидывать исключение, делов-то.
      • +4
        Исключения для такого случая всё-таки сомнительная практика (по крайней мере в PHP).
        • –1
          Доктрина при попытке найти ентити по ключу кидает исключение. И это правильно

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

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


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


            И это правильно. Поиск по ключу операция семантически отличная от, например, получения значения в массиве по ключу. Для поиска результат «не найден» — не исключение.
      • 0
        Как уже выше отметили для некоторых случаев исключения — не лучший выход. А вот null все-таки очень универсальный тип, означающий «ничто».
        Давайте рассмотрим примеры, когда корректно выбрасывать исключение, а когда все-таки лучше null(примеры очень субъективны, буду рад конструктивной критике):
        • Корректнее вернуть null, например, при работе с ORM, например, какой-нибудь абстрактный Model::findByPk($id). При этом лучше, если у нас будет возможность указать хинтом, экземпляр какого класса должен быть возвращен методом findByPk
        • Доступ к элементу связанного списка по индексу(хотя кто же на php использует связанные списки!). В этом случае лучше выбросить исключение. При этом возвращаемые значения могут быть определенного типа либо же любого типа.
        • +1
          Не соглашусь: если вы говорите точно, что этот метод должен вернуть SomeClass, то он уже не просто МОЖЕТ, а ДОЛЖЕН вернуть его. И если у метода нет такой возможности, то это исключительная ситуация. Так же как и при передаче параметра в функцию, если вы передадите null вместо SomeClass, то получите «Argument 1 passed to some_func() must be an instance of SomeClass, null given».
          Если вы добавляете строгости своему коду, то нужно ее соблюдать, а если хотите null, то не надо закладывать строгость.
          Мне интуитивно понятен именно такой подход, не понимаю полустрогости.
          • +1
            Ваш ник намекает, что вам ближе maybe-подход к обработке подобных случаев.
            Но в php это получится дороговато по ресурсам.

            >> если вы говорите точно, что этот метод должен вернуть SomeClass
            речь о том, что я хочу сказать, что этот метод должен вернуть SomeClass_или_null
            • +1
              Мой ник совсем не связан с моими пристрастиями :-)

              Я понял, вам надо это https://wiki.php.net/rfc/nullable_types
              Ну я не против, если в таком виде, но предпочитаю очевидную строгость.
          • 0
            Так же как и при передаче параметра в функцию, если вы передадите null вместо SomeClass

            Мы передаём не SomeClass, а указатель на объект класса SomeClass. Так что null вполне уместен.
            • 0
              Ок, передайте в своем php null в функцию с type-hinting-ом, и не получите fatal error.
              • +1
                Я про это и говорю, что null должен передаваться. Почему этого не сделали — не знаю.
                • +1
                  разные школы кунг-фу
          • +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»
          • +2
            Возвращение объекта определенного класса или null (вернее какого-то специального значения, будь то null, NullObject, false и т. п.) во многих случаях вполне нормальная ситуация, а не исключительная, например тот же поиск по уникальному ключу в репозитории — для репозитория это просто «не найден», а исключительная это ситуация (договор ссылается на несуществующего клиента, например) или нормальная (раз не найден, то надо создать нового) решается одним, как минимум, уровнем выше, клиентом репозитория. Но уж если возвращает не признак «не найден», то должен вернуть объект строго определенного класса. И это не полустрогость, а вполне нормальная строгость «или ..., или ...».
            • 0
              К сожалению это все последствие зимней драмы с тайп хинтингом. Пока все холиварили RFC добавляющее нулабл как-то упустили
              • +2
                Это понятно. Я к тому, что бросать исключения, если не можешь вернуть полноценный объект не потому, что не смог его создать, хотя от тебя это ожидалось, а потому, что не смог его найти в хранилище или ещё где — порочная практика в общем случае. И вернуть какое-то особое значение — нормально. А null это будет, особый инстанс обычного класса, обычный инстанс особого наследника обычного класса, обычный инстанс особого класса с тем же интерфейсом, что обычный класс — детали реализации, зачастую диктуемые языком.
    • +5
      Запрет на null должен быть в языках с подержкой maybe типов и сопоставления с образцом. Там это очень лаконично вписывается. В php же это проблема, которая уже на рассмотрении, просто это не успели включить в версию. Ждем nullable значения в 7.1
  • +1
    рантайму и другим разработчикам ценную информацию о том, что вы пытались сделать, без необходимости исполнять код.

    Рантайм обрабатывает как дополнительную проверку типа(type-hinting), это не полноценная типизация. Так что на оптимизации расчитывать не стоит.
    • +1
      Тут вы не совсем правы. Один из разработчиков обещал после принятия указания скалярных типов, написать JIT для PHP, и во многом он будет основываться на type-hinting.
      Так что ждём что у него получится.
      • 0
        Вы про ветку zend-jit?

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

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

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

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

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

            А никто и не занимается. К этому скорее всего вернутся после стабилизации и релиза PHP7, глядишь в каком-нибудь PHP8 уже будет JIT.
            • 0
              информация о типах есть, так что предполагалось что PHP код написанный в стрикт режиме можно просто компилить напрямую в машинный код.

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

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

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

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

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

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

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

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

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


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

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

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.