Статьи

Можем ли мы иметь статические типы в PHP без PHP 7 или HHVM?

Теперь, когда PHP 7 уже давно вышел с интересными функциями, такими как обработка ошибок, оператор объединения нулей, объявления скалярных типов и т. Д., Мы часто слышим, как люди по-прежнему придерживаются PHP 5, говоря, что у него слабая система типирования, и что все быстро стать непредсказуемым.

Векторная иллюстрация рабочего стола программиста

Хотя это отчасти верно, PHP позволяет вам контролировать свое приложение, когда вы знаете, что делаете. Давайте посмотрим несколько примеров кода, чтобы проиллюстрировать это:

function plusone($a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone("1"));
var_dump(plusone("1 apple"));
 // output

int(2)
int(2)
int(2)

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

 function plusone($a)
{
    if ( !is_numeric($a) )
    {
        throw new InvalidArgumentException("I can only increment numbers!", 1);
    }

    return $a + 1;
}

Это вызовет InvalidArgumentException Если мы указываем желаемый тип в прототипе функции…

 function plusone(int $a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone("1"));
var_dump(plusone("1 apple"));
 // output

PHP Catchable fatal error:  Argument 1 passed to plusone() must be an instance of int, integer given, called in /vagrant/test_at/test.php on line 7 and defined in /vagrant/test_at/test.php on line 2

Эта ошибка поначалу кажется немного странной, потому что первый вызов нашей функции использует целое число!

Если мы внимательно прочитаем сообщение, то увидим, что в сообщении об ошибке указано «должен быть экземпляр типа int» — предполагается, что целое число является классом, поскольку PHP до версии 7 поддерживал только подсказки типов классов!

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

Расширенные типы

Перед выпуском PHP 7 команда Box предложила отличную идею для решения проблемы безопасности при печати в своем приложении PHP 5. После использования утверждений, подсказок и т. Д. Они решили поработать над более чистым решением этой проблемы.

Мы видели, как Facebook продвинул PHP немного вперед, запустив HHVM и Hack , но команда Box не хотела разветвлять исходный код PHP или что-либо изменять в ядре. Их решение состояло в том, чтобы создать отдельное расширение, называемое расширенными типами, для анализа типов phpDoc и assert метода во время выполнения.

Установка

Приведенное ниже расширение предназначено для использования с PHP 5 — если вы используете PHP 7, просто расслабьтесь и наслаждайтесь поездкой!

Процесс установки не очень сложен и не требует какой-либо конкретной настройки. Приведенные ниже инструкции относятся к Ubuntu, но легко применимы и к другим * nix-системам.

 # update system
sudo apt-get update

# install required dependencies
sudo apt-get install php5-dev bison flex

# clone the repo
git clone [email protected]:box/augmented_types.git

# install extension

phpize
./configure --enable-augmented_types
make
make test
sudo make install

Мы должны указать PHP загрузить наше расширение, отредактировав файл php.ini

 # Get php.ini location from PHP info. This will print the cli configuration file, you may need to edit /etc/php5/fpm/php.ini )
php -i | grep 'Loaded Configuration File'

# output: /etc/php5/cli/php.ini
vim /etc/php5/cli/php.ini
 # Get `extension_dir` the PHP info
php -i | grep extension_dir
# output: extension_dir => /usr/lib/php5/20100525 => /usr/lib/php5/20100525

# Add this line at the end of the file
zend_extension=/usr/lib/php5/20100525/augmented_types.so

Расширение можно включить для каждого файла с помощью функции ini_set

 ini_set("augmented_types.enforce_by_default",1);

Или непосредственно в файле конфигурации php.ini Обязательно ознакомьтесь с документацией для получения более подробной информации о процессе установки.

использование

Ранее мы упоминали, что расширение использует phpDoc для прототипов функций / методов. Большая часть его функциональности может быть объяснена с помощью примеров кода.

 /**
 * Add one
 *
 * @param   int $a
 * @return    int
 */
function plusone($a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone("1"));
var_dump(plusone("1 apple"));

Вы можете подумать, что сможете угадать вывод приведенного выше кода, но вы, вероятно, ошибаетесь!

 int(2)
PHP Fatal error:  Wrong type encountered for argument 1 of function plusone, was expecting a integer but got a (string) '1' in /vagrant/test_at/test.php on line 15

Даже второй звонок не проходит! Это происходит потому, что мы не выполняем преобразование PHP, о котором мы говорили ранее, функция требует от нас строго передавать целое число. А как насчет того, чтобы вызывать его с помощью float?

 var_dump(plusone(1.5));
 PHP Fatal error:  Wrong type encountered for argument 1 of function plusone, was expecting a integer but got a (float) 1.500000 in /vagrant/test_at/test.php on line 14

Мы заставили нашу функцию принимать два типа аргументов (int и float), используя определение составных типов .

 /**
 * Add one
 *
 * @param   int|float $a
 * @return    int
 */
function plusone($a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone(1.5));

Теперь наша функция должна работать как положено.

 int(2)
PHP Fatal error:  Wrong type returned by function plusone, was expecting a integer but got a (float) 2.500000 in /vagrant/test_at/test.php on line 0

К сожалению! Мы также должны определить тип возвращаемого значения для нашей функции или привести приведенное значение в соответствие с phpDoc.

 /**
 * Add one
 *
 * @param   int|float $a
 * @return    int|float
 */
function plusone($a)
{
    return $a + 1;
}

Мы также можем работать с составными типами: в следующем примере суммируются элементы массива.

 /**
 * Calculate sum
 *
 * @param   array $nums
 * @return    int
 */
function sum($nums)
{
    $sum = 0;
    foreach ($nums as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum([10, 12, 76]));

// output
int(98)

Функция возвращает ожидаемое значение. Что если массив содержит что-то кроме целых чисел?

 var_dump(sum([10, 12, "something"]));

// output
int(22)

Расширение дает нам возможность работать с типами массивов . Предыдущий пример выдаст фатальную ошибку, как и ожидалось.

 /**
 * Calculate sum
 *
 * @param   int[] $nums
 * @return    int
 */
function sum($nums)
{
    $sum = 0;
    foreach ($nums as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum([10, 12, 76]));
var_dump(sum([10, 12, "something"]));
 int(98)
PHP Fatal error:  Wrong type encountered for argument 1 of function sum, was expecting a (integer)[] but got a array in /vagrant/test_at/test.php on line 20

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

 /**
 * Calculate sum
 *
 * @param   *int $nums
 * @return    int
 */
function sum($nums)
{
    $args = func_get_args();
    $sum = 0;
    foreach ($args as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum(10, 12, 76));

Определение типа *int Мы можем объединить определения типов *intint[]

 /**
 * Calculate sum
 *
 * @param   *int|int[] $nums
 * @return    int
 */
function sum($nums)
{
    if ( !is_array($nums) )
    {
        $nums = func_get_args();
    }

    $sum = 0;
    foreach ($nums as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum(10, 12, 76));
var_dump(sum([10, 12, 76]));

Теперь оба вызова функций вернут одно и то же значение ( int(98)

Аргументы по умолчанию

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

 /**
 * SSH to server.
 *
 * @param   string      $url
 * @param   int|void    $port
 * @return  bool
 */
function ssh($url, $port = 2222)
{
    return true;
}

Примечание . Если аргумент имеет значение по умолчанию, он не будет применять указанный тип. Это означает, что передача строки в аргументе порта не приведет к ошибке.

Типы возврата

То же самое, что мы говорили ранее о типах аргументов, применимо к определениям возвращаемых типов. Мы можем указать нормальные скалярные типы, классы, составные типы и типы массивов. Каждая функция должна иметь определение @return <type>должна использовать @return void

 class User
{
    protected $id;

    protected $name;

    /**
     * Constructor
     * @param int       $id
     * @param string    $name
     * @return void
     */
    public function __construct($id, $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    /**
     * Return the user info as a string.
     *
     * @return    string
     */
    public function __toString()
    {
        return $this->name;
    }
}

Игнорирование файлов

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

 augmented_types_blacklist([__DIR__."/vendor"]);

Расширение теперь будет игнорировать наш каталог поставщиков. Если у нас есть некоторые из наших собственных проверяемых каталогов внутри каталога vendor, мы можем использовать функцию белого списка, чтобы добавить их обратно. Вы можете прочитать больше об этом в документации .

 augmented_types_whitelist([__DIR__."/vendor/mylib"]);

Мы также можем достичь того же результата, используя файл php.ini

 # php.ini
augmented_types.whitelist=./vendor
augmented_types.blacklist=./vendor/mylib

PHP 7 Изменения

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

Если PHP 7 уже поддерживает это, что может быть полезным при установке нового расширения?

Ну, расширение предоставляет широкий спектр опций, которых нет в PHP 7, например:

  • Составные типы ( @param string|int $var
  • Типы массивов ( @param int[] $var
  • Принуждение функций к возврату значения (по крайней мере, void

Вывод

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

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

Что вы думаете о расширении? Это полезно? У вас есть другие идеи о том, как это сделать? Нам любопытно узнать, что вы думаете, дайте нам знать!