Pull to refresh

Новый PHP, часть 2: Scalar types

Reading time8 min
Views45K
Original author: Larry Garfield

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

До сих пор мы говорили о типах только в отношении классов и интерфейсов. В течение многих лет мы только их (и массивы) и могли использовать. Однако же, PHP 7 добавляет возможность использовать и скалярные величины тоже, такие как int, string и float.

Но постойте. В PHP большинство примитивов являются взаимозаменяемыми. Мы можем передать "123" в функцию, которая хочет int, и довериться PHP, который все сделает «правильно». Так для чего же тогда нужны скалярные типы?

Также как и типы возвращаемых значений, скаляры увеличивают ясность кода, дают возможность поймать больше ошибок на раннем этапе. Что, в свою очередь, повышает надежность кода.

PHP 7 добавляет четыре новых типа, которые могут быть заданы параметрами или возвращаемым значениям: int, float, string и bool. Они присоединятся к уже существующим array, callable, классам и интерфейсам. Давайте дополним наш предыдущий пример с учетом новой возможности:

interface AddressInterface {
  public function getStreet() : string;
  public function getCity() : string;
  public function getState() : string;
  public function getZip() : string;
}

class EmptyAddress implements AddressInterface {
  public function getStreet() : string { return ''; }
  public function getCity() : string { return ''; }
  public function getState() : string { return ''; }
  public function getZip() : string { return ''; }
}

class Address implements AddressInterface {
  protected $street;
  protected $city;
  protected $state;
  protected $zip;

  public function __construct(string $street, string $city, string $state, string $zip) {
    $this->street = $street;
    $this->city = $city;
    $this->state = $state;
    $this->zip = $zip;
  }

  public function getStreet() : string { return $this->street; }
  public function getCity() : string { return $this->city; }
  public function getState() : string { return $this->state; }
  public function getZip() : string { return $this->zip; }
}

class Employee {
  protected $id;
  protected $address;

  public function __construct(int $id, AddressInterface $address) {
    $this->id = $id;
    $this->address = $address;
  }

  public function getId() : int {
    return $this->id;
  }

  public function getAddress() : AddressInterface {
    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(int $id) : Employee {
    if (!isset($this->data[$id])) {
      throw new InvalidArgumentException('No such Employee: ' . $id);
    }

    return $this->data[$id];
  }
}

$r = new EmployeeRepository();

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

Изменения коснулись некоторых методов, отдающих строку или число. Даже сделав такой простой шаг, мы уже получили некоторые преимущества.

  1. Теперь известно, что различные поля класса Address являются просто строками. Раньше можно было только предполагать, что они были строками, а не объектами Street (состоящими из номера улицы, ее названия и номера квартиры) или ID города из базы данных. Конечно, обе эти вещи совершенно разумны в определенных обстоятельствах, но в данной статье они не рассматриваются.
  2. Известно, что идентификаторы сотрудников — целые числа. Во многих компаниях ID сотрудника является алфавитно-цифровой строкой или, возможно, номером с лидирующим нулем. Раньше не было способа узнать, теперь же иные трактовки исключены.
  3. Плюсом является и безопасность. Мы гарантированно знаем, что внутри findById() $id — это значение типа int. Даже если оно изначально пришло из пользовательского ввода, оно станет целочисленным. Это означает, что оно не может содержать, например, SQL-инъекции. Опора на проверку типов при работе с пользовательским вводом не единственная, и даже не лучшая защита от нападения, но еще один слой защиты.

Кажется, что первые два преимущества избыточны при наличии документации. Если у вас есть хорошие doc-блоки в коде, вы уже знаете, что Address состоит из строк и ID сотрудника является целочисленным, верно? Это правда; однако, не все придерживаются фанатичности в вопросе документирования своего кода, или же просто забывают обновить ее. С «активной» информацией из самого языка вы гарантированно будете знать, что рассинхронизации нет, ведь PHP выбросит исключение, если это не так.

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

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

function loadUser(int $id) : User {
  return new User($id);
}

function findPostsForUser(int $uid) : array {
  // Obviously something more robust.
  return [new Post(), new Post()];
}

$u = loadUser(123);

$posts = findPostsForUser($u);

loadUser() всегда возвращает объект типа User, а findPostsForUser() всегда возвращает integer, нет никакой возможности сделать этот код верным. Об этом можно сказать лишь взглянув на функции и способ их использования. А это, в свою очередь, означает, что и IDE тоже знает заранее и может предупредить нас об ошибке до запуска. И поскольку IDE может отслеживать намного больше частей, чем мы, то она может и предупреждать о большем количестве ошибок, чем можно заметить самостоятельно… при этом не исполняя код!

Этот процесс называется «статический анализ», и он является невероятно мощным способом оценки программ с целью поиска и исправления ошибок. Этому немного мешает стандартная слабая типизация PHP. Передача целого числа в функцию, которая ожидает строку, или строку в функцию, ожидающую целое, все это продолжает работать и PHP молча конвертирует примитивы между собой, также как и всегда. Что делает статический анализ, нами или с помощью утилит, менее полезным.

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

По умолчанию, при работе со скалярными типами (параметрами или возвращаемыми значениями), PHP будет делать все возможное, чтобы привести значение к ожидаемому. То есть, передача int в функцию, ожидающую string будет прекрасно работать, а передавая bool при ожидаемом int вы получите целое число 0 или 1, потому что это естественное, ожидаемое от языка поведение. У объекта, переданного в функцию, ожидающую string, будет вызваться __toString(), тоже самое произойдет и с возвращаемыми значениями.

Единственным исключением является передача строки в ожидаемый int или float. Традиционно, когда функция рассчитывает получить значения int/float, а передается string, PHP просто будет обрезать строку до первого нечислового символа, в результате чего возможно потеря данных. В случае со скалярными типами, параметр будет работать нормально, если строка действительно является числовой, но если же значение будет усекаться, то это приведет к вызову E_NOTICE. Все будет работать, но на текущий момент такая ситуация рассматривается как незначительная ошибка в условии.

Авто-преобразование имеет смысл, в тех случаях, когда практически все входные данные передаются как строки (из базы данных или http-запросы), но в то же время оно ограничивает полезность проверки типов. Как раз для этого в PHP 7 предлагается strict_types режим. Его использование является несколько тонким и неочевидным, но при должном понимании разработчик получает невероятно мощный инструмент.

Чтобы включить режим строгой типизации, добавьте объявление в начало файла, вот так:

declare(strict_types=1);

Это объявление должно быть первой строкой в файле, до выполнения какого-либо кода. Оно затрагивает только логику, расположенную в файле и только вызовы и возвращаемые значения в этом файле. Чтобы понять как работает strict_types, давайте разобьем наш код на несколько отдельных файлов и немного его изменим:

// EmployeeRespository.php

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(int $id) : Employee {
    if (!isset($this->data[$id])) {
      throw new InvalidArgumentException('No such Employee: ' . $id);
    }
    return $this->data[$id];
  }
}

// index.php

$r = new EmployeeRepository();

try {

  $employee_id = get_from_request_query('employee_id');

  print $r->findById($employee_id)->getAddress()->getStreet() . PHP_EOL;
}
catch (InvalidArgumentException $e) {
  print $e->getMessage() . PHP_EOL;
}

Что же можно гарантировать? Самое главное, мы наверняка знаем, что $id внутри findById() является int, а не строкой. Неважно, чем будет $employee_id, $id всегда примет тип int, даже если будет выброшен E_NOTICE. Если мы добавим декларацию strict_type в EmployeeRepository.php, то ничего не произойдет. Мы все также будем иметь int внутри findById().

Однако, если объявить использование режима строгой типизации в index.php, а затем там же использовать вызов findById(), то в случае если $employee_id будет являться строкой, или float или чем-то другим кроме int, то это приведет к выбросу TypeError.

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

Так что же в этом хорошего? Проницательный читатель может заметить, что я сделал очень коварный баг в последнем примере кода. Проверьте конструктор EntityRespository, там где мы создаем наши фейковые данные. Вторая запись передает ZIP-код как int, не строку. Большую часть времени это не будет иметь никакого значения. Тем не менее, в США почтовые коды на северо-востоке начинаются именно с ведущего нуля. Если ваш int начинается с ведущего нуля, PHP будет интерпретировать это как восьмеричное число, то есть число с основанием 8.

При слабой типизации это означает, что в Бостоне адрес будет интерпретироваться не как zip-код 02113, а как целое число 02113, что по основанию 10 будет: 1099, вот его-то PHP и переведет в почтовый индекс «1099». Поверьте мне, жители Бостона ненавидят это. Такую ошибку в итоге можно отловить где-то в базе или при валидации, когда код принудительно заставит ввести именно шестизначное число, но в тот момент вы и понятия не будете иметь откуда пришло 1099. Может быть позднее, часов через 8 отладки, будет понятно.

Вместо этого, мы переключим EntityRespository.php в strict-режим и сразу же поймаем несоответствие типов. Если запустить код, то получим вполне конкретные ошибки, которые скажут нам точные строки, где их искать. А хорошие утилиты (либо IDE) могут поймать их еще до запуска!

Единственное место, где режим строгой типизации позволяет автоматические преобразования — из int в float. Это безопасно (кроме случаев с экстремально большими или малыми значениями, когда есть последствия переполнения) и логично, поскольку это int по определению также является и значением с плавающей точкой.

Когда же мы должны использовать строгую типизацию? Мой ответ прост: как можно чаще. Скалярная типизация, типы возвращаемых значений и strict-mode предлагают огромные преимущества для дебаггинга и поддержки кода. Все они должны использоваться максимально, и как результат, будет более надежный, поддерживаемый и безглючный код.

Исключением из этого правило может быть код, который напрямую взаимодействует с каналом входных данных, таким как базы данных или входящие http-запросы. В этих случаях входные данные собираются всегда быть строками, потому что работа в интернете это просто более проработанный способ конкатенации строк. При ручной работе с http или БД можно переводить данные из строк в необходимые типы, но обычно это выливается в большое количество ненужной работы; если SQL-поле имеет тип int, то вы знаете (даже если не знает IDE), что из него будет всегда отдаваться числовая строка и, следовательно, сможете быть уверены в безопасности и отсутствии потерь данных при передаче их в функцию, ожидающую int.

Это означает, что в нашем примере EmployeeRespository, Employee, Address, AddressInterface и EmptyAddress должны иметь включенный strict-режим. index.php же взаимодействует с входящими запросами (через вызов get_from_request_query()), и таким образом, вероятно, легче будет доверить PHP разбираться с типами, а не делать это вручную самостоятельно. Как только неопределенные значения из запроса передадутся в типизированную функцию, тогда-то и можно переключаться в строготипизированную работу.

Будем надеяться, что переход на PHP 7 будет намного быстрее, чем как это было с PHP 5. Это действительно стоит того. Одной из главных причин как раз и является расширенная система типов, дающая нам возможность сделать код более самодокументированны и более понятным друг другу и нашим инструментам. В результате получится намного меньше «хммм, я даже и не знаю что с этим делать» моментов, чем когда-либо прежде.
Tags:
Hubs:
+19
Comments94

Articles

Change theme settings