PHP: значения по-умолчанию и типизация

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

<?php

class Product
{
    /**
     * @param int $productId
     *
     * @return int
     */
    public function getStrangeProduct(int $productId): int
    {
        return $productId;
    }
}

$obProduct = new Product();

При таком раскладе совершенно очевидно, что вызов

var_dump($obProduct->getStrangeProduct(0));

вернёт целочисленное значение 0:

/mnt/projects/sites/server.local/www/test-functions.php:16:int 0

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


    /**
     * @param int|null $productId
     *
     * @return int
     */
    public function getStrangeProduct(?int $productId = 100500): int
    {
        return $productId;
    }

Это позволит успешно выполнить функцию без указания параметра и входной параметр будет инициализирован значением по-умолчанию:

var_dump($obProduct->getStrangeProduct());

На выходе:

/mnt/projects/sites/server.local/www/test-functions.php:18:int 100500

Но также появилось и нежелательное поведение: входной параметр теперь может принимать значение NULL. Получается, то значение NULL и неуказанное значение с точки зрения входного параметра имеют одинаковую проверку типа (пустота она и есть пустота). Но вот значение по-умолчанию уже не присваивается! Значение по-умолчанию делается только для случая, когда ничего не передано. Такой вызов:

var_dump($obProduct->getStrangeProduct(null));

уже "упадёт", но не напроверке входного параметра, а на проверке выходного: мы объявили, что возвращать будем int, а возвращается NULL:

TypeError: Return value of Product::getStrangeProduct() must be of the type int, null returned in /mnt/projects/sites/server.local/www/test-functions.php on line 12

Значение по-умолчанию не подхватилось! Ведь на вход было передано значение NULL - это не тоже самое, как если бы совсем ничего не передавать!

Чтобы уложиться в ТЗ, придётся явно указать значение по-умолчанию самостоятельно:


    /**
     * @param int|null $productId
     *
     * @return int
     */
    public function getStrangeProduct(?int $productId = 100500): int
    {
        if (null === $productId) {
            $productId = 100500;
        }
        return $productId;
    }

Вот теперь всегда будет возвращаться целочисленное значение: и при $obProduct->getStrangeProduct(), и при $obProduct->getStrangeProduct(null).

Но для совсем хорошего бизнес-решения, эту функцию ещё нужно доработать. Тут название функции намекает, что идёт работа с неким товаром. В подавляющем большинстве реальных случаев, программист имеет дело с сущностями из базы данных. Идентификаторы в базе данных почти всегда автоматически инкрементируются. И часто начиная с единицы. Т.е. товара с ID = 0 обычно не бывает (но это не точно - зависит от движка сайта, настройки СУБД и пр.). Поэтому лучше добавить в функцию также проверку входящего значения для учёта бизнес-логики:


    /**
     * @param int|null $productId
     *
     * @return int|null
     */
    public function getStrangeProduct(?int $productId = 100500): ?int
    {
        $result = null;
        if (null === $productId) {
            $result = 100500;
        } elseif ($productId > 0) {
            $result = $productId;
        }

        return $result;
    }

Тут появляются варианты: что делать, когда входное значение не удовлетворяет требованиям бизнеса? Можно кидать исключение. А можно расширить возвращаемое значение и возвращать NULL - во всех случаях, когда невозможно вычислить верное зачение. Так и сделано в примере выше. Внимание! в этом случае во всём коде, где будет производится вызов этой функции, придётся делать проверку возвращённого значения - если вернётся NULL, значит функция не отработала по корректному сценарию.

Но даже на этом этапе функция может "упасть" в неожиданном месте. Можно сложить два целочисленных числа, передать на вход и получить исключение:

var_dump($obProduct->getStrangeProduct(PHP_INT_MAX + 1));

Это приведёт к переполнению максимально допустимого целочисленного типа. PHP увеличит размерность числа (до float) и "упадёт" на проверке входного значения (которое у нас int):

TypeError: Argument 1 passed to Product::getStrangeProduct() must be of the type int or null, float given, called in /mnt/projects/sites/server.local/www/test-functions.php on line 25 in /mnt/projects/sites/server.local/www/test-functions.php on line 10

PHP попытался превратить входное значение в 9.2233720368548E+18.

А что будет, если принудительно сделать приведение входного значения к целочисленному типу?

var_dump($obProduct->getStrangeProduct((int) (PHP_INT_MAX + 1)));

Это вернёт:

/mnt/projects/sites/server.local/www/test-functions.php:25:null

А всё потому, что после приведение типа, на вход в функцию будет подано значение -9223372036854775808 - это то, что получилось после прибавления единицы к самому большому поддерживаемому челому числу. Да, получилось отрицательное число! И наша функция отработала по проверке бизнес-логики - должно быть положительного входное значение.

В общем, нужно иметь ввиду, что на очень больших числах можно словить переполнение. А отрицательные числа лучше сразу фильтровать на входе - если они реально не нужны.

Комментарии

Популярные сообщения из этого блога

Пропорциональное распределение суммы

Битрикс: своя геолокация

Bitrix24 API - разбор демо приложения третьего типа