В интернете часто можно встретить вопрос, нужно ли передавать NULL в параметрах методов, а также возвращать NULL из методов. Если нет, то почему и как писать код в таких случаях? Об этом и пойдёт речь в статье.

В чём вообще проблема использования null значений? Дело в том, что NULL можно трактовать по разному — это когда значение неизвестно, его используют в качестве значений по умолчанию для параметров методов, возвращают когда не смогли получить успешный результат или когда что-то произошло не так. Есть куча способов и причин использовать NULL. Однако это не всегда приемлемо.

Передача null в параметры

Передавать NULL в параметры метода может быть полезно, если нужно указать отсутствие значения или инициализировать параметр с нулевым значением по умолчанию.

Например, предположим, что у вас есть метод, который принимает параметр $name:

<?php

public function greet($name) {
    echo "Hello, $name!";
}

Если вы хотите обрабатывать случаи, когда он не указан, вы можете установить для параметра $name значение по умолчанию NULL:

<?php

public function greet($name = null) {
    if ($name === null) {
        echo "Hello, World!";
    } else {
        echo "Hello, $name!";
    }
}

В этом случае, если вы вызываете greet() метод без передачи каких-либо аргументов, он примет для параметра $name значение NULL по умолчанию.

Причина отказа от NULL заключается в том, что не нужно постоянно его проверять, если вы ожидаете в значении какое-то другое содержимое. Вместо этого вы можете вернуть, пустой массив, если ожидаемый результат — это массив. Если это целое число, возможно, будет достаточно 0, если это строка, то пустая строка и так далее. 

Но есть и другие способы передачи и проверки аргументов метода. К примеру, можно передавать аргументы в виде массива:

<?php

function getParams(array $params) {
    return [
        'columns' => $buttonParameters['col'],
        'rows' =>  $params['count']
        'count' => $params['count']
    ];
}

Другой вариант, не указывать аргументы функции явно, а воспользоваться динамическими аргументами. В этом поможет функция func_get_args(). И в теле уже самой функции просто валидировать получаемые функцией аргументы:

<?php

function getParamsFunction()
{
    var_dump(func_get_args());
}

getParamsFunction(1, 2, 3, 7, 10, 12);

Дополнительно можно использовать функции func_num_args() и func_get_arg().

Однако эти два способа сильно ухудшают понимание кода и требует множество дополнительных проверок каждого параметра. Поэтому эти способы создают ещё больше хлопот, чем использование NULL по умолчанию. Тем не менее эти способы частенько можно встретить.

В PHP 8 появились именованные аргументы. Теперь передать параметры можно следующим образом:

<?php

function params($a = 1, $b = 2) {
    print "a: $a, b: $b";
}

params(b: 5); // Выведет "a: 1, b: 5"

В этом случае аргументы функции передаются по имени параметра, а не по его позиции. Аргумент не зависит от порядка и указанного значения по умолчанию. Именованный аргумент передаётся с помощью добавления двоеточия перед значением имени параметра. Для имени параметра можно использовать зарезервированные слова. Однако имя не может быть создано динамически и должно быть идентификатором.

Возврат null из метода

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

К примеру, возьмем такой метод:

<?php

function getClients()
{
  $query = 'SELECT * FROM clients';
  $result = $this—>mysqli—>query($query);

  if (!$result) {
    return false;
  }

  $data = [];

  while ($row = $result->fetch_assoc())
  {
    $data[] = $row;
  }


  return $data;
}

В нём мы собираемся получить список клиентов из базы данных. Если клиенты успешно получены, мы возвращаем массив, а если что-то пошло не так, то метод возвращает false. Затем мы используем данный метод, чтобы далее проводить операции с клиентами:

<?php

$result = $db—>getClients();

Вот здесь и начинаются первые сложности, поскольку метод возвращает несколько типов данных: array и false. Поэтому нужно дополнительно определить, что именно было возвращено. Вроде бы здесь нет ничего такого необычного и проверка данных может показаться обычным делом, но здесь  как раз возможно ошибиться на проверке типа данных.

<?php

$result = $db—>getClients();

if (!$result) {
// что-то произошло не так
}
// работаем с массивом клиентов

В этом примере идёт неявная проверка данных, поэтому даже если функция getClients() возвращает пустой массив, то условие сработает. Поэтому лучше проверять на конкретный тип данных:

<?php

$result = $db—>getClients();

if ($result === false) {
// что-то произошло не так
}

if (count($result) === 0) {
 // пустой результат
}

// а если не false и array не пустой, то работаем с массивом клиентов

Если мы используем вместо false другое значение, пустую строку или NULL, то ситуация может возникнуть практически такая же. Как в таком случае лучше изменить код и что использовать?

Вместо возврата false, пустой строки или NULL можно использовать исключения. Это позволит сразу же определить, что именно и на каком моменте произошло.

<?php

function getClients()
{
  $query = 'SELECT * FROM clients';
  $result = $this—>mysqli—>query($query);

  if (!$result) {
    throw new ErrorException('Произошла такая-то ошибка');
  }

  $data = [];

  while ($row = $result->fetch_assoc())
  {
    $data[] = $row;
  }


  return $data;
}

Затем при использовании метода обработать это исключение:

<?php

try {
  $result = $db—>getClients();
  // все норм, двигаемся дальше
} catch (ErrorException $e) {
  // схватили ошибку!
}

Здесь уже не нужно дополнительно проверять на всякие false или NULL и можно быть точно уверенным, почему метод не отдал правильные данные. Важно лишь правильно перехватывать и обрабатывать такие исключения.

<?php

public getFileSize(File $file) {
  if (!$file—>exists()) {
    return null;
  }
  
  return $file—>getLength();
}

В этом простом примере метод возвращает размер файла. Если файла не существует, то возвращается NULL, 0 или пустая строка — неважно. Вот здесь и начинается проблема, что файла не существует, он был перемещён или удалён, но вместо того чтобы узнать об этом, подобная проверка маскирует проблему. В этом случае можно использовать исключение:

<?php

public getFileSize(File $file) {
  if (!$file—>exists()) {
     throw new Exception(
      "Нельзя получить размер файла. Файл не найден!"
    );
  }
  
  return $file—>getLength();
}

Начиная с PHP7 для функций можно установить тип возвращаемого значения. Например:

<?php

function getValue(): int {
  return 777;
}

Результат:

int(777)

Если указать цифру строкой:

<?php

function getValue(): int {
  return '777';
}

То она автоматически будет преобразована в int.

Результат:

int(777)

Чтобы включить строгую типизацию, нужно установить:

declare(strict_types=1);

Тогда если попытаться задействовать в этой функции строку, будет выдана ошибка:

Uncaught TypeError: Return value of getValue() must be of the type integer, string returned

Начиная с PHP 7.1 можно указать тип возвращаемого значения, допускающий значение NULL. Для этого нужно поставить вопросительный знак перед типом возвращаемого значения. Таким образом, функция может вернуть целое или нулевое значение.

<?php

function getValue() : ?int {
  return null;
}

Кроме этого, если в методе вообще не использовать return или просто указать его без всякого значения, метод вернёт NULL. Например:

<?php

function getValue1(){
  return;
}

function getValue2() {
}

Поскольку return;и return null; эквивалентны в PHP, то если возвращаемое значение не указано, PHP выполнит работу NULL за вас. То же самое касается и свойств объектов. По умолчанию они имеют значение NULL до тех пор, пока не будут заданы. Поскольку это поведение по умолчанию, лучше помнить об этом и проверять свойства, которые имеет ваш класс, прежде чем совершать вызовы или что-то ещё.

В большинстве случаев рекомендуется возвращать Null-object который предоставляет поведение по умолчанию в зависимости от того как реагирует ваш код на ситуацию когда ничего не найдено.

Так что же в итоге возвращать?

Все индивидуально, зависит от проекта. Главное следовать установленной спецификации в рамках проекта, чтобы соблюдался единый стиль. Проверка нулевых значений — обычная практика, иначе вы рискуете повсюду получить исключения. Лучше корректно обработать ошибку, чем генерировать исключения, когда в этом нет необходимости. NULL это не обязательно плохо, как и в случае со многими другими решениями, это зависит от ситуации.

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

Плохое использование NULL, это когда идет попытка заменить или скрыть исключительные ситуации, такие как: ошибка добавления записи в бд, подключение к какому-то API, неверные параметры и так далее. В этих случаях создание исключения более адекватно, поскольку значение NULL не дает значимой информации об ошибке и любые проверки игнорируют проблему.

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


  1. plutarh
    21.10.2023 09:25
    +1

    Монада Maybe и/или Either как вариант входного и/или возвращаемого типа.


  1. dopusteam
    21.10.2023 09:25
    +6

    Странно возвращать false из метода getClients, тут пустой массив самый адекватный вариант. Так что пример, вокруг которого добрая половина статьи строится - неудачный.

    Либо бросать исключение, если там к бд например не удалось подключиться. Но никак не false

    if ($result) {
        throw new ErrorException('Произошла такая-то ошибка');
      }

    Тут кажется проверка неправильная.

    Ещё не хватает совета именовать методы в соответствии с результатом, например, не getClient, а getClientOrNull.


    1. condor-bird Автор
      21.10.2023 09:25
      +2

      Возможно вы невнимательно прочитали) В примере действительно показано, что возвращается false. Затем как раз и объясняется, что вместо того, чтобы просто возвращать false, можно задействовать исключение, массив, строку - в зависимости от того, что ожидаем получить.

      P.S. Проверку поправил, видимо редактор съел некоторые куски. Насчет именования методов хороший совет.


      1. SuperCat911
        21.10.2023 09:25

        Конечно, есть такое поверье, что наиболее профессиональное поведение - это падать при первой же ошибке. Однако, такое поведение не всегда уместно. Например, может быть неудобно если ваш сервер получает пакетные запросы (rpc), то есть должен вызвать несколько методов, которые должны быть запущены независимо от предыдущих результатов.

        Мне нравится как в VK API сделано, когда возвращается объект {error: null|{...}, response:null|{...}, success: true|false} (на память писал, может там немного не так, но идея правильно передана).

        Поэтому получать false вместо массива вполне себе нормально при условии, что можно понять была ли ошибка.


        1. FanatPHP
          21.10.2023 09:25
          +1

          Все верно, наружу должен торчать JSON. Вот только внутри самого API получать этот JSON как раз удобно с помощью исключений. Падать при первой ошибке (для которой это оправдано) и ловить в обработчике исключений, который тупо вернет стандартный JSON {error: true, response:null, success: false}


          1. SuperCat911
            21.10.2023 09:25

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

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

            В моем коде на PHP обычно возвращается массив (который в ответе сервера затем переведу в JSON)

            {
            ...
              success: false,
              result: false,
              error {
                code: "RANGE_ERROR_VALUE_IS_TOO_LARGE",
                description: "\"{num}\" is too large, maximum is \"{max}\""
                min: 0,
                max: 10,
                current: 12
              }
            ...
            }

            Код ошибки придуман не только для классификации, а также для перевода на различные языки на стороне клиента.

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

            Если настаиваете на работе с нативными исключениями в PHP, то как тогда лучше быть в этом случае? Получается так, что какие-то функции могут бросать исключения, а какие-то возвращать массив. Хочется как-то более унифицировано.


            1. FanatPHP
              21.10.2023 09:25
              +2

              Во-первых, да не настаиваю я :)
              Я же с вами не спорю. А наоборот - согласился, в самом начале своего комментария. И просто дополнил. Где там настояние? Я же говорю не "обязательно без вариантов", а "гораздо удобнее". И действительно - ну удобнее жи. Для тех же фатальных ошибок, типа отсутствия соединения с базой данных.

              Опять же, я оговорился: "для которой это оправдано". То есть варианты разные.

              В своем примере, как я понял, вы говорите про валидацию входящих данных. А это не то что исключение - это даже вообще не ошибка. Само по себе наличие некорректных данных - это не ошибка. Это ошибка данных, но не программы. Важно понимать это различие. Для программы валидации (как и для контроллера) это нормальное, предусмотренное поведение: получили на вход данные, поработали с ними, вернули результат. Функция должна кидать исключение, если она не может выполнить свою работу. Если при валидации входящих данных кончилась например память - это исключение. Если функция валидации отработала нормально, а данные некорректные - это не исключение.

              А дальше уже два варианта.

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

              Если же какую-то валидацию делает модель (что вполне имеет смысл на самом деле, чтобы не дублировать проверки), то она может кинуть исключение. Потому что если функция сохранения данных не может это сделать - вот это уже исключительная ситуация. Но тут опять же, модель может кинуть исключение определенного типа. И обработчик исключений может автоматом преобразовывать исключения этого типа в человекопонятные ответы. И волки сыты, и овцы целы.

              Если вернуться к статье, то оба исключения там оправданы. Это не валидация входящих данных, а ошибка самой программы. Если функция для обращения к БД не смогла выполнить свою работу - это исключительная ситуация (вот только исключение должно кидаться не кривыми ручками программиста как в статье, а самим драйвером БД). А дальше уже такое исключение гораздо удобнее обрабатывать централизованно. Но только обработчик исключений в этом случае не должен передавать клиенту никакие детали, а просто выдать обобщенное сообщение, типа "Ошибка сервера, попробуйте позже".


              1. SuperCat911
                21.10.2023 09:25
                +1

                Спасибо! Дали хорошую пищу для размышления.


    1. MihaOo
      21.10.2023 09:25
      +5

      Не все об этом знают, но в PHP сообществе принято именовать методы, которые возвращают НЕ nullable как get<Something>, при этом они выбрасывают исключение если это самое <Something> не найдено. Если же мы возвращаем nullable, такие методы именуют как find<Something>.


  1. plFlok
    21.10.2023 09:25
    +2

    Статья вызывает спорные чувства.
    Концовка содержит правильные советы.

    Начало - какой-то набор вредных советов под тезисом "так тоже можно делать".

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

    Не надо такому учить. Магические методы типа func_get_args для функций, чьи сигнатуры вообще не предполагают аргументов, напрочь ломают статический анализ кода. Кроме того это может ухудшить навигацию по коду в ide, так как теряются типы переданных в функцию аргументов, find usages отрабатывает не всегда.

    Однако в некоторых случаях, возврат NULL как и возврат других значений,
    которые изначально не подразумевались для возврата, могут приводить к
    ошибкам и непонятным замешательствам

    и эти ошибки прекрасны! Они сразу показывают, что код зашёл в непредусмотренное состояние, и требуется внимание, чтоб переписать его на кооректное поведение. Если автор функции указал в возвращаемых типах nullability - извольте обработать это значение. Если не обработали - сами себе злые буратино.
    И вопрос nullability возвращаемого значения решается на этапе написания изначальной функции. Возвращать в случае ошибок пустую строчку вместо null только для того, чтоб код коллег-злых-буратин не падал на несовпадении типа со строковым - это какое-то заметание проблем под ковёр.
    Если ошибки хочется просто не видеть, а не исправлять, то можно выключить варнинги кастов прямо в настройках интерпретатора, не декларировать strict_types, и php сделает за вас то же самое - сам превратит null-ы в строки, и даже не будет фонить в stderr. Но так не делают уже лет 5, потому что потом больно.

    На одном старом проекте было прикольно, когда запросы в базу всегда выглядели как

    select email from t where email is not null and email != ""
    потому что накастовали к пустым строкам. Зато stderr пустой.


    1. condor-bird Автор
      21.10.2023 09:25

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

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

      и эти ошибки прекрасны!

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

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

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

      Если ошибки хочется просто не видеть, а не исправлять, то можно выключить варнинги кастов прямо в настройках интерпретатора, не декларировать strict_types, и php сделает за вас то же самое - сам превратит null-ы в строки, и даже не будет фонить в stderr. Но так не делают уже лет 5, потому что потом больно.

      Это верно.

      На одном старом проекте было прикольно, когда запросы в базу всегда выглядели как

      select email from t where email is not null and email != ""

      А вот это частый случай)


  1. mcavalon
    21.10.2023 09:25

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

    Плохо:

    function methodName(): bool|array|null
    {...}
    
    $result = $class->methodName();
    
    if($result === false) {
    ....
    }
    
    if($result === null) {
    ....
    }
    
    dd($result);

    Хорошо:

    function methodName(): array
    {
        // код
    
        throw new Exception('ошибка');
    
        return [];
    }
    
    try {
        $result = $class->methodName();
    } catch (Exception) {
    }
    
    dd($result);

    Чисто имхо.


    1. FanatPHP
      21.10.2023 09:25
      +3

      Вы вслед за автором повторяете это голословное утверждение, в котором совершенно нет логики. "На мой взгляд лучше покрывать код try-ями. Тогда не будет необходимости покрывать код if-ами." ШТА?
      Вы сами не видите здесь юмора? :)

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

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

      try {
          $result = $class->methodName();
      } catch (NullResultException) {
         ...
      } catch (FalseResultException) {
         ...
      }

      И сразу пример перестает быть наглядным пособием, почему "один try лучше двух if-ов".

      Польза исключения не в том, что его можно сразу поймать. В этом случае как раз не будет никакой разницы, if это, или try. А в том, что его можно поймать где-то ещё.

      Но повторюсь, основная проблема вашей рекомендации в том, что она валит в кучу как "пустой" результат, так и ошибку.

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


      1. condor-bird Автор
        21.10.2023 09:25

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

        Простой пример тому, что на каком-то запросе идет запись в базу данных, а также загрузка файла на сервер. Плюсом тут же в try/catch делаем begin/commit/roleback для бд. Причин, по которым что-то здесь может пойти не так - много.

        В остальном, по поводу тройного типа и примера обработки несколько catch, согласен.


        1. FanatPHP
          21.10.2023 09:25
          +1

          Не, посыл статьи был в том, чтобы не покрывать фанатично все try,

          Значит это еще один минус за сумбурность. Потому что этот посыл не читается от слова "никак".

          Плюс вы все равно сбиваетесь на "покрывать try/catch" :)

          Всем любителям "покрывать" я всегда задаю один простой вопрос: учитывая, что ошибка в require ломает приложение, у вас лично в коде все require завернуты в try-catch?


    1. SuperCat911
      21.10.2023 09:25

      Я выше писал о своей практике возвращать из определенного вида функций структурированный ответ в виде массива. Однако, необязательно код нужно обвешивать if-ами. Если возвращается ошибка, то работу скрипта можно прервать с помощью кастомной функции. Пример кода:

      $note_id = 123;
      
      // возвращает объект {result, error}
      $delete_note_resp = db_notes_deleteByNoteId($note_id);
      die_if_error_response($delete_note_resp);
      
      // далее, логика работы с результатом
      

      Поэтому это вполне нормально, что результат может быть false или null в каких-то случаях. Только тут нюанс в том, есть ли обработка ошибок до работы с этим result.


      1. FanatPHP
        21.10.2023 09:25

        Функция конечно лучше, чем if, но она точно так же засоряет код. Зачем вообще её вызывать (да и писать), если можно обойтись без неё?


        1. SuperCat911
          21.10.2023 09:25

          Можно, однако...

          У меня нет разделения ошибок на "ошибки данных" и "ошибки программы". Грубо говоря, все что нужно сказать пользователю "красным шрифтом", будь то неправильный логин/пароль или "service internal error" - это я называю ошибкой (мое понимание мира :) ).

          Слои модели, контролера (валидация данных) генерируются моим кодогенератором. Сервисный слой (это подслой контроллера) генерируется частично, потому что в нем пишется вся бизнес-логика. Поэтому код функций довольно унифицирован, как и возращаемые форматы значений. То есть все сгенерированные будут возвращать объект с полями error и result. Нет винегрета в возвращаемых форматах. Также я упоминал, что ошибки - это тоже объект, который имеет поля (параметры). Выше приводил пример функции, которая проверяет выход за границы числа. Мне нужно вернуть в браузер тип ошибки, какие границы и какое число проверялось.

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

          И также могу упомянуть о том, что die_if_error_response($delete_note_resp); - запись короткая и не загрязняет код. Даже сохраняет чистоту кода и отвечает на вопрос: а что будет если где-то возникнет исключение.


  1. Bigata
    21.10.2023 09:25
    +2

    Так и не понял чем возврат null лучше false


    1. gun_dose
      21.10.2023 09:25
      +2

      Если возвращаемый тип nullable, то удобно использовать conditional chaining:

      $email = $this->getUser()?->getEmail();

      А если бы getUser возвращал в случае пустого значения false, пришлось бы писать более громоздкую конструкцию.


  1. FanatPHP
    21.10.2023 09:25
    +4

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

    В принципе, у меня у самого такое же клиповое мышление. Но я борюсь с этим вычиткой. Я просто читаю свой собственный текст на предмет, есть ли в нем какой-то смысл. И исправляю. Сначала отдельные куски, а потом проверяю согласованность отдельных участков текста между собой.

    Причина отказа от NULL заключается в том, что не нужно постоянно его проверять, если вы ожидаете в значении какое-то другое содержимое. Вместо этого вы можете вернуть, пустой массив, если ожидаемый результат — это массив.

    Извините, я не понимаю, как эти два предложения связаны между собой. Вроде бы вы говорили о параметрах, при чем здесь возврат? Такое ощущение, что этот абзац попал сюда из следующего раздела. Ну ОК, можем вернуть пустой массив, makes sense. Запомним это.

    В целом, раздел "Передача null в параметры" вызывает скорее недоумение. Какой-то винегрет из передачи null, передачи неизвестного количества параметров, и передачи необязательных параметров (которые совсем необязательно должны быть null). Именованные параметры как раз про последнее.

    Раздел "Возврат null из метода" опять же вызывает вопросы, именно по части связности.

    Вот здесь и начинаются первые сложности, поскольку метод возвращает несколько типов данных: array и false.

    Постойте, но ведь только что вы предлагали возвращать пустой массив? И данная конкретная проблема тут же исчезает. Но дальше вдруг выясняется, что проблема совсем не в этом. Зачем тогда её вообще упоминать?

    Вместо возврата false, пустой строки или NULL можно использовать исключения.

    А вот здесь уже всё плохо. Каким местом тут исключение? С каких пор запрос, который не вернул записей - это исключительная ситуация? И каким местом 5 строчек с try-catch лучше трех с условием? В чем смысл? Зачем вообще использовать здесь исключение? Какие-то голословные утверждения на уровне анекдота

    -- исключение лучше чем условие!
    -- чем лучше?
    -- чем условие!

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

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

    Исключение должно кидаться в случае, если функция не в состоянии выполнить свою работу.

    Это очень простое правило, по которому можно очень легко проверять свой код. Функция получения данных из БД смогла получить данные? Смогла. То, что БД не вернула никаких строк - это не её проблема. Исключение здесь не нужно.

    Да, есть случаи, когда результат должен быть обязательно, но это специальный случай, который обычно отражается в названии метода, классическоеfindOrFail() например. Но это ведь совсем другой случай. Который, внезапно, упоминается в заключении, но никак не следует из приведенного примера.

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

    Но в нем добавлена классическая бессмысленная проверка, которая ухудшает качество кода. Допустим, файл есть, но нет прав на чтение. Что произойдет в этом случае? Правильно, getLength()кинет исключение! Как и в случае, если файл не найден. Зачем тогда писать эту проверку? И выдавать абсолютно неинформативный текст вместо системного сообщения об ошибке, которое содержит тонну полезной информации, в частности путь, по которому мы пытались прочитать файл или какие-то другие важные подробности. Всегда надо дважды подумать, добавлять ли бессмысленную пользовательскую проверку там, где РНР и так проинформирует об ошибке.

    Заключение в целом верное, но как оно следует из остального текста - совершенно неясно.


    1. Mausglov
      21.10.2023 09:25
      +1

      Функция получения данных из БД смогла получить данные? Смогла. То, что БД не вернула никаких строк - это не её проблема. Исключение здесь не нужно.

      В примере из статьи как раз не смогла, а если никаких строк не было - вернётся пустой массив.


      1. FanatPHP
        21.10.2023 09:25

        Эх, вот я растяпа. Спасибо.
        Буду сейчас опровержение писать.


    1. FanatPHP
      21.10.2023 09:25

      Поправка: я проглядел, что false тут возвращается по делу. Но обработка исключения по месту всё портит. Весь смысл исключений в том, что их можно поймать где-то еще. А если ловить сразу, то это ничем не будет отличаться от return null/false с последующей проверкой.

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

      function getClients()
      {
          $query = 'SELECT * FROM clients';
          return $this->mysqli->query($query)->fetch_all(MYSQLI_ASSOC);
      }

      И вызываем его тоже без лишних проверок:

      $result = $db—>getClients();
      // все норм, двигаемся дальше

      Повторюсь, именно в этом заключается преимущество исключений. А не в том, что мы поменяли шило на мыло if на try.


      1. condor-bird Автор
        21.10.2023 09:25

        Согласен, все верно. Но почему в примере описал именно так, ниже в комментарии ответил.


    1. condor-bird Автор
      21.10.2023 09:25

      Извините, я не понимаю, как эти два предложения связаны между собой. Вроде бы вы говорили о параметрах, при чем здесь возврат? Такое ощущение, что этот абзац попал сюда из следующего раздела. Ну ОК, можем вернуть пустой массив, makes sense. Запомним это.

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

      А вот здесь уже всё плохо. Каким местом тут исключение? С каких пор запрос, который не вернул записей - это исключительная ситуация? И каким местом 5 строчек с try-catch лучше трех с условием? В чем смысл? Зачем вообще использовать здесь исключение? Какие-то голословные утверждения на уровне анекдота

      Если конкретно за этот пример, то согласен, можно было не выбрасывать исключение:

      $result = $this—>mysqli—>query($query);
      
      if (!$result) {
        throw new ErrorException('Произошла такая-то ошибка');
      }
      

      Но только в том случае, если сама query() обертка нормальная и не возвращает тот же false, а то, что уже можно через try отловить.


  1. vitiok78
    21.10.2023 09:25

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

    Мне больше нравится подход Go, когда ошибка - это одно из возвращаемых значений, которое надо обработать прямо на месте. В PHP можно использовать union type в типе возвращаемого значения, где будет указан тип нормального ответа и тип ошибочного ответа. Вместо специального типа для ошибки можно использовать тот самый null.

    В общем, главная задача - обработать ошибку сразу в месте вызова. Это значительно улучшит качество кода. А как это будет сделано: при помощи исключения либо при помощи возвращаемого значения (даже и null), - уже не так важно. Главное - стандартизировать этот подход в проекте.


    1. FanatPHP
      21.10.2023 09:25

      Вы просто не умеете их готовить.

      Если 99% ошибок обрабатываются совершенно одинаково, то имеет все-таки смысл как-то оптимизировать этот процесс и не засорять код однообразными проверками.


    1. Tony-Sol
      21.10.2023 09:25

      Если нравится порочная (отчасти это всего лишь шутка) практика go возвращать ошибку, то да, через union types можно сделать нечто подобное

      <?php
      function devide (int|float $a, int|float $b): int|float|Throwable {
      	if ($b == 0) {
      		return new Exception('Devide by zero');
      	}
      	return $a/$b;
      }
      
      var_export(devide(4, 2));
      echo PHP_EOL;
      var_export(devide(4, 0));
      
      ...
      
      $res = devide($someA, $someB);
      if ($res instanceOf Throwable) {
      	echo 'something went wrong';
      }
      
      ....
      
      2
      \Exception::__set_state(array(
         'message' => 'Devide by zero',
         'string' => '',
         'code' => 0,
         'file' => '/home/user/scripts/code.php',
         'line' => 5,
         'trace' => 
        array (
          0 => 
          array (
            'file' => '/home/user/scripts/code.php',
            'line' => 12,
            'function' => 'devide',
            'args' => 
            array (
              0 => 4,
              1 => 0,
            ),
          ),
        ),
         'previous' => NULL,
      ))

      но зачем©


      1. vitiok78
        21.10.2023 09:25

        Я считаю саму концепцию исключений весьма плохой практикой. Это разновидность магии, а магия в программировании - это всегда плохо.

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

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

        Необязательно возвращать Throwable) Можно возвращать нужный вам конкретно в этом месте тип ошибочного значения. Это может быть и false, и null, и даже специальный объект


        1. Tony-Sol
          21.10.2023 09:25
          +1

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

          Просто сам гошный подход вида

          if res, err := foo(bar); err != nil {
            return nil, err
          }

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

          Исключения я могу ловить по нужному мне типу, фильтровать до какого уровня по стеку оно выбросится и т.д. В гошке конечно тоже можно обмазать все errors.Is() или errors.As() , как

          res, err := foo(bar)
          if errors.Is(err, OneKindOfError) {
              ...
          } else if errors.Is(err, OtherKindOfError) {
              ...
          } else if errors.As(err, SomePackageWideError) {
              ...
          } else {
              ...
          }

          но это не настолько удобно (если такое слово вообще можно применить) как

          try {
            $res = foo($bar);
          } catch (OneKindOfException $e ) {
              ...
          } catch (OtherKindOfException $e) {
              ...
          } catch (\Throwable $t) {
              ...
          }

          именно по причине выше - это все нужно дублировать во всей цепочке вызовов, а не точечно ловить в нужных местах.

          А ведь это (откладывать обработку ошибки) может быть полезно и удобно, например, когда ваш пакет является прокси/"миддлварой"/пред-пост обработчиком какого-нибудь 3rd patry пакета - внутри конвертирует ошибки разных типов и приводит к единому интерфесу (вымученный пример конечно).

          Вообще, после общения с гошниками которые уверяют что это самый прям труЪ-вей работы с ошибками, хочется спросить - вот python, емнип, с момента своего появления поддерживает множественные возвращаемые значения, так почему эта практика так и не прижилась раньше, до go? почему остались и используются raise и try/except, может быть это реально не настолько удобно как нам пытаются продать?


  1. koreychenko
    21.10.2023 09:25

    Тот, кто писал эту статью он застрял в PHP 5.6?
    Особенно радует абзацы, вроде:

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

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

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

    public function greet($name = null) {
        if ($name === null) {
            echo "Hello, World!";
        } else {
            echo "Hello, $name!";
        }
    }
    1. Почему у аргумента не указан тип string? Про тип возвращаемого значения тоже есть вопросы.

    2. С точки зрения доменной логики может ли быть ситуация, что у пользователя нет имени? Что там у нас там со схемой в базе, например. Может ли у нас в базе name быть nullable?

    3. Если юзер не указал своего имени зачем нам его пытаться приветствовать? (Бизнес-решение)

    Что пытаюсь донести:
    Типизация решает одну простую проблему в этом случае: если метод получил параметр и вызвался (в том смысле, что приложение не упало по дороге), то этот метод точно сможет с этим параметром что-то полезное сделать. Ну и тестировать каждый конкретный метод будет сильно проще.

    Еще пример:

    <?php
    
    function getParams(array $params) {
        return [
            'columns' => $buttonParameters['col'],
            'rows' =>  $params['count']
            'count' => $params['count']
        ];
    }

    Здесь нет гарантий, что ключи массива вообще присутствуют в этом $params. Можно использовать какой-нить: DTO объект и тогда быть гарантированными:

    <?php
    
    function getParams(ParamsDto $params): array {
        return [
            'columns' => $params->getCols(),
            'rows' =>  $params->getRows(),
            'count' => $params->getCount(),
        ];
    }

    Часто всякие $params используют для настройки (например подключение к базе, всякие валидаторы и т.п.) В таком случае обычно они используются как способ переписываения каких-то дефолтных параметров объекта. Т.е. даже если в массиве нет определенного ключа - в дальнейшем будет браться значение по-умолчанию.

    Если врубить доменную логику, то возврат пустого объекта из репозитория вместо null это, конечно, бред :-) Вы засовываете руку в коробку и ожидаете там либо найти один мячик, либо рука вернется пустой. Либо объект, либо null. Всё как в жизни.
    Когда вы ожидаете несколько объектов, то вы ожидаете что-то по чему итерировать. Можно, конечно, вместо коллекции объектов вернуть null, но зачем? Никто не работает с коллекцией как с чем-то цельным. Обычно все равно итерируют и работают с каждым объектом в коллекции отдельно.


    1. condor-bird Автор
      21.10.2023 09:25

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

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

      В PHP 8 появились именованные аргументы

      Вот это вас ведь не смутило)

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

      Если коснулись версии, то это вообще не показатель. Много проектов продолжают использовать 7.4 и даже ниже. В один присест никто на 8.* все не перенесёт.

      1. Если юзер не указал своего имени зачем нам его пытаться приветствовать? (Бизнес-решение)

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


      1. koreychenko
        21.10.2023 09:25

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

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

        С моей точки зрения, если по бизнес логике метод умеет писать перед именем пользователя Hello то без имени пользователя он не имеет смысла, следовательно, никакого nullable параметра там быть не может.