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

Type hinting


В PHP7 появились подсказки типов (type hinting), что позволило IDE проводить более качественный статический анализ кода, качество нашего кода улучшилось (или правильно говорит "стало более лучше"? ).

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

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

Но кроме приятных возможностей type hinting накладывает и обязанности, то есть типы переменных действительно должны быть такими как указано в сигнатуре метода.

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

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

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

Работаешь конечно с массивами (например когда читаешь из *.csv), работать с базой можно через ORM, но для моих задач это слишком громоздко, мне удобно работать с базой через PDO, которое отдаёт тебе данные опять же в массивах. «Любимый» Bitrix не умеет возвращать данные иначе как в массиве.

Как ни крути приходиться извлекать данных из массивов. Поэтому я написал обёртку для работы с массивами.

Что бы не копипастить код из проекта в проект я оформил пакет для Composer:

composer require sbwerewolf/language-specific

ValueHandler


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

И теперь нужны методы для приведения типа, типов в PHP не много, получились такие методы:

  1. int()
  2. str()
  3. bool()
  4. double()

Иногда попадаются массивы, поэтому пусть будет и для массивов:

  • array()

Иногда надо просто получить элемент как он есть:

  • asIs()

Иногда элемента с заданным индексом может не быть и тогда надо использовать значение по умолчанию:

  • default()

ArrayHandler


Следующим требованием было получить возможность упростить массив из одного значения до ровно этого значения.

Покажу на примере из документации:

$connection = new PDO ($dsn,$login,$password);

$command = $connection->prepare('select name from employee where salary > 10000');
$command->execute();
$data = $command->fetchAll(PDO::FETCH_ASSOC);
/*
$data =
    array (
        0 =>
            array (
                'name' => 'Mike',
            ),
        1 =>
            array (
                'name' => 'Tom',
            ),
        2 =>
            array (
                'name' => 'Jerry',
            ),
        3 =>
            array (
                'name' => 'Mary',
            )
    );
*/
$names = new ArrayHandler($data);
$result = $names->simplify();

echo var_export($result,true);
/*
LanguageSpecific\ArrayHandler::__set_state(array(
   '_data' => 
  array (
    0 => 'Mike',
    1 => 'Tom',
    2 => 'Jerry',
    3 => 'Mary',
  ),
))
*/

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

$response[] = $element[0];

, но мне так не нравится, пусть это происходит автоматически, так появился метод simplify().

Ну и раз уж у нас есть обёртка над массивом, то добавим метод для проверки наличия индекса — has(), если захочется пробежаться по элементам массива, то поможет метод next().

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

Выглядит это так:

$address = new ArrayHandler($item)->pull('metaDataProperty')->pull('GeocoderMetaData')->pull('Address')->asIs();

Можно конечно и так писать:

$address = $item['GeoObject']['metaDataProperty']['GeocoderMetaData']['Address'];

, но у меня в глазах рябит от количества квадратных скобок, мне удобней через pull().

Общие рассуждения


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

Перед тем как делать свой пакет я посмотрел аналоги и ни чего подобного не нашёл, есть несколько проектов, которые просто обёртка над array, и в этих проектах просто оборачивают многие методы для работы с массивами, а типобезопасности ни где нет.

Видимо написать (int) или (bool) перед именем переменной всем просто и удобно и ни кто не видит смысла заморачиваться с отдельным репозиторием под это дело.

Возможности библиотеки чуть шире описанных в статье и больше информации можно получить в документации (README.md).

PHP5 ещё не редкость, поэтому у библиотеки есть отдельная версия для PHP5, отличается от версии для PHP7 названием нескольких методов и конечно весь type hinting только в коментах.
Есть версия библиотеки для PHP7.2, отличается только тем что в сигнатуре у метода object() появляется тип возвращаемого значения — object.

Код полностью покрыт тестами, но в принципе так и ломать не чему :)

Пользуйтесь на здоровье!

Ещё один пример использования


foreach ($featureMember as $item) {
    $pointInfo = extract($item);
    $info = new ArrayHandler($pointInfo);

    $address = $info->get('formatted')->default('Челябинск')->str();
    $longitude = $info->get('longitude')->default(61.402554)->double();
    $latitude = $info->get('latitude')->default(55.159897)->double();

    $undefined = !$info->get('formatted')->has();

    $properties = ['longitude' => $longitude, 'latitude' => $latitude, 'address ' => $address ,'undefined'=>$undefined,];
    $result = json_encode($properties);
    output($result);
}

Смотреть во время отладки на JSON в котором числа это числа, логические значения — логические, мне намного приятней чем только на строки.

image

А вам как?

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


  1. vn_sten
    16.11.2019 17:34

    Вы смотрели коллекции в ларавель?


    1. SbWereWolf Автор
      16.11.2019 17:35

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


      1. dspancov
        16.11.2019 18:35

        1. SbWereWolf Автор
          16.11.2019 21:47

          Спасибо за ссылку.
          Посмотрел на коллекции. Весь функционал коллекций это работа с массивом. Но основное назначение моей библиотеки это работа со значением.
          Во первых понять определено оно или нет.
          Во вторых получит значение строго заданного типа, либо получить значение по умолчанию.
          Этот функционал не сложно сочинить самому, занимает он пять строчек кода, но когда тебе это надо сделать для 5 — 15 колонок, тебе очень хочется упихать обработку одной колонки в одну строчку и не страшно если это будет цепочка вызовов, и не страшно что под капотом будет работать не 5, а 500 сторок кода, это всё не имеет значения когда тебе просто надо накидать код, который жить будет от силы неделю, а скорей всего уже завтра утратит свою актуальность.



  1. vn_sten
    16.11.2019 17:46

    я не предлагал вам писать парсеры используя ларавель, просто то что вы написали умеет делать (и даже больше) коллекции которые вы могли бы выдрать из ларавель, а по теме ключами массива не могут быть float, они преобразуются в int =)


    1. SbWereWolf Автор
      16.11.2019 18:41

      Про float, верное замечание.


      1. GreedyIvan
        17.11.2019 17:54

        А если использовать генераторы, то и это ограничение можно снять


  1. E_STRICT
    16.11.2019 20:02
    +1

    В PHP7 появились подсказки типов (type hinting)
    Всё таки, правильней переводить «type hinting» как «контроль типов». И появился он не в PHP 7, а в PHP 5. В PHP 7 был добавлен контроль скалярных типов и возвращаемых значений. Кроме этого в следующем релизе (7.4) появятся типизированные свойства объектов.


  1. E_STRICT
    16.11.2019 20:16

    Следующим требованием было получить возможность упростить массив из одного значения до ровно этого значения.
    Для этого есть специальный режим.
    phpdelusions.net/pdo/fetch_modes#FETCH_COLUMN


  1. E_STRICT
    16.11.2019 20:24

    мне удобно работать с базой через PDO, которое отдаёт тебе данные опять же в массивах
    А как же PDO::FETCH_OBJ и PDO::FETCH_CLASS?


    1. SbWereWolf Автор
      16.11.2019 22:49

      PDO::FETCH_CLASS — Зачем такие сложности? Специфика задач подразумевает очень короткий срок жизни кода. И даже безотноситено этого, как использование PDO::FETCH_CLASS гарантирует мне соблюдение типов?
      Автоматического приведения типов не случиться, присвоения значений по умолчанию тоже.
      Писать каждый раз логику? вот я и написал библиотеку один раз что бы использовать везде. Будь у меня источник данных PDO или .csv, или json из какого то API, не важно какой источник данных, если он сводиться к массиву то с помощью своей библиотеки у меня всегда на выходе будут переменные строго заданных типов.
      FETCH_COLUM по этой же причине не всегда применим, по этой же причине методу simplify можно отдать массив с нужными нам индексами и он вернёт только заданные колонки, это нужно когда мы из
      .csv файла парсим не все колонки, а только две три.


      1. E_STRICT
        17.11.2019 08:56

        PDO::FETCH_CLASS — Зачем такие сложности? Специфика задач подразумевает очень короткий срок жизни кода. И даже безотноситено этого, как использование PDO::FETCH_CLASS гарантирует мне соблюдение типов?
        Не совмем представляю, что такое короткий срок жизни кода. Классы как раз упрощают работу с данными и предоставляют типизацию. Если вы не можете использовать ORM, создавайте простые value-объекты через PDO::FETCH_CLASS или из массивов.

        final class User {
          public function getName(): string {
            return $this->name;
          }
        }


        См. steemit.com/php/@crell/php-use-associative-arrays-basically-never

        FETCH_COLUM по этой же причине не всегда применим
        array_column


        1. SbWereWolf Автор
          17.11.2019 11:13

          Идея с классами вполне себе альтернативный вариант, но надо гетеры выписать, это чуть сложней чем просто одна строка get('side_numbers')->int().
          И в базу писать массив как то проще, с классом придётся этот массив предварительно создать и наполнить.
          array_column() — да, спасибо за подсказку, можно использовать внутри simplify().
          Короткий срок жизни кода это когда нам надо завести в базу исходные данные которые позже будут обработаны с помощью SQL.
          Моя последняя задача была о том что бы из csv залить в базу точки продаж и с помощью описательного адреса для каждой точки определить гео координаты.
          А дальше конечные пользователи CRM с точками продаж должны работать через админку Битрикса.
          И кругом у тебя массивы — csv — массив, ответ геокодера — массив, ответ ORM Битрикса — массив.
          Соответственно с обработкой этих данных и появлялись новые требования к этой библиотеке и исходя из них развивался функционал.


          1. genteelknight
            18.11.2019 14:19

            ORM Битрикса же умеет объекты возвращать


            1. SbWereWolf Автор
              18.11.2019 14:38

              Я нашёл два метода:

              1. CIBlockElement::GetPropertyValues
              2. CIBlockElement::GetPropertyValuesArray

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

              Как получить объект?


              1. genteelknight
                18.11.2019 14:43

                В новом ядре есть ORM, которая ± сносная и умеет отдавать объекты.

                dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&CHAPTER_ID=011687


  1. olezh
    16.11.2019 23:28

    Очень плохо написано по-русски


    1. SbWereWolf Автор
      17.11.2019 00:12
      -1

      Главное что бы код был ок. А пресс релизы пусть пресс секретари пишут и прочие контент менеджеры. Каждоиу своё


      1. koeshiro
        17.11.2019 00:42

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


        1. SbWereWolf Автор
          17.11.2019 01:15

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


      1. olezh
        17.11.2019 02:40

        То есть ты сам для себя написал статью, ок


        1. SbWereWolf Автор
          17.11.2019 09:54
          +1

          Кому надо разберётся. Или вообще публиковать ни чего не надо если стиль излржения кому то может показаться не достаточно доходчивым ?


      1. Hett
        18.11.2019 08:13

        Первое что бросилось в глаза, это метод next() который возвращает генератор. Дальше даже смотреть не стал.



  1. olegmar
    17.11.2019 10:27

    Зачем ArrayHandler->simplify(), когда есть array_column?


    1. SbWereWolf Автор
      17.11.2019 12:33

      Смысл использования ArrayHandlera в том что по get() он выдаёт ValueHandler у которого есть методы для приведения типов.
      Можно упрощать массив с помощью array_column(), но для типобезопасности всё равно надо создать экземпляр ArrayHandler.
      Смысл использования библиотеки в интеграции функционала.
      Можно писать $number = (int)@(vars['key']) всегда на выходе будет int и ни когда не будет ошибки "ключ не найден", но как то не очень нравиться мне такой код.


  1. ReDev1L
    17.11.2019 15:24

    В паре с fractal.thephpleague.com/transformers ваша либа дает жару, спасибо.


  1. BeMySlaveDarlin
    17.11.2019 15:25

    Исходя из задачи в начале, можно было обойтись опциями для PDO fetch и json encode/decode, если вопрос касался только скалярных типов.


  1. Hatsu
    17.11.2019 15:25

    foreach ($featureMember as $item) {
        $pointInfo = extract($item);    
    
        $address = (string) ($pointInf['address'] ?? 'Челябинск');
        $longitude = (float) ($pointInfo['longitude'] ?? 61.402554);
        $latitude = (float)  ($pointInfo['latitude'] ?? 55.159897);
    
        $undefined = !array_key_exists('formatted', $pointInfo);
    
        $properties = ['longitude' => $longitude, 'latitude' => $latitude, 'address ' => $address ,'undefined'=>$undefined,];
        $result = json_encode($properties);
        output($result);
    }
    

    Мне кажется тут можно обойтись без библиотеки


    1. ReDev1L
      17.11.2019 15:29

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


    1. SbWereWolf Автор
      17.11.2019 15:50

      можно обойтись, а можно не обойтись, здесь:

      $pointInf['address']

      будет лишний варнинг если индекса нет.
      Прелесть библиотеки в автодополнении, жмёшь контрл + пробел и выбираешь вариант, на клавиатуре, за секунду, обычно IDE подставляет первыми вариантами то что тебе надо и для того что бы набить код уходит минимум времени, часто пишешь на одном дыхании.
      Можно писать так:
      $val1 = (int)(array_key_exists('key1', $vars) ? $vars('key1') : $def1);
      $val2 = (int)(array_key_exists('key'2, $vars) ? $vars('key2') : $def1);
      $val3 = (float)(array_key_exists('key3', $vars) ? $vars('key3') : $def2);
      $val4 = (bool)(array_key_exists('key4', $vars) ? $vars('key4') : $def3);
      $val5 = (int)(array_key_exists('key5', $vars) ? $vars('key5') : $def1);
      

      а мне нравиться писать так:
      $vars = new ArrayHandler($vars);
      $val1 = $vars->get('key1')->default($def1)->int();
      $val2 = $vars->get('key2')->default($def1)->int();
      $val3 = $vars->get('key3')->default($def2)->double();
      $val4 = $vars->get('key4')->default($def3)->bool();
      $val5 = $vars->get('key5')->default($def1)->int();
      

      и не надо ни каких ребусов решать с "??" и когда дочитал всю строку выражения не надо вспоминать что результат надо привести к типу который был указан в начале строки.
      Всё делается линейно:
      $val1 = 
      // в $val1 запиши значение следующего выражения
      $vars
      // из массива $vars
      ->get('key1')
      // возьми элемент с индексом 'key1'
      ->default($def1)
      // используй значение по умолчанию $def1
      ->int();
      // приведи значение к int
      

      Мне такой код легче заходит, библиотека написана для себя и мне не жалко ей поделиться, кто может обойтись без неё обходитесь, кто же вас неволит?


      1. GreedyIvan
        17.11.2019 16:13

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


        ?? кинет ворнинг при отсутствии элемента с таким ключом — это фиаско.


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


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


      1. GreedyIvan
        17.11.2019 16:22

        За велосипед с default хочется отправить почитать что-нибудь про Option. Не говоря уже о том, что ValueHandler объединяет в себе и конструктор объекта, и трансформацию, и хранилище. Жуткий класс. Хочется отдать его трем разным людям, чтобы каждый сделал свою часть правильно.


        1. SbWereWolf Автор
          17.11.2019 17:09

          ValueHandler это хранилище, которое может отдать значение приведённое к нужному типу, может выдать тип этого значения, может показать флаг было ли значение задано.
          Конструктор объекта в ValueHandler отсутствует, это не фабрика.
          У меня не было цели сделать 100500 классов, мне по функционалу хватило двух классов (на самом деле четырёх).
          Разделять на классы имеет смысл если планируется активно менять функционал. У меня таких планов нет.
          Ради искусства я только прикрутил фабрику для ValueHandler, ещё что то лепить ради красоты уже не хочется.
          Но вы можете сделать пул реквест с вашей правильной реализацией.

          И кстати можете дать ссылку на «любой сериалайзер», думаю благодарен за неё буду не я один.
          И ссылку на почитать про Option.

          С методом default() что не так? обычный сеттер, магия в геттерах для значения:

          // значение имеется ? 
          $result = $this->has() ?
          // да, вернуть значение
           $this->_value
          // нет, вернуть значение по умолчанию
          : $this->_default;
          

          в чём криминал?


          1. GreedyIvan
            17.11.2019 18:05

            Всё это делается средствами языка. Пример уже приводили. (type) — это каст. ?? — альтернатива, если null.
            null ===
            gettype / is_ — для получения и проверки типов.


            А криминал в том, что для случае null вы, зачем-то, прогоняете альтернативное значение через объект, делая его интерфейс крайне нетривиальным. Например, в каком состоянии будет объект после второго применения default? Можно ли так вообще делать? Вызывая метод default, я откуда-то могу знать, что это объект больше никто не будет использовать? И т.д.


            1. SbWereWolf Автор
              17.11.2019 19:37

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

              Куда что я прогоняю? Я устанавливаю свойство экземпляра, это свойство используется при выдаче значения.
              Вызывая метод default, я откуда-то могу знать, что это объект больше никто не будет использовать?

              А кто код пишет? если вы ссылку на экземпляр отдадите наружу, то с её помощью экземпляром можно будет воспользоваться. Библиотека почему должна этому препятствовать?
              Мне вообще не понятно почему библиотека должна как то ограничивать своего пользователя? можно создать экземпляр с каким то значением и получить его приведённым к чему угодно и сколько угодно раз, и каждый раз перед получением значения можно устанавливать разные значения по умолчанию, почему нет?
              Если хочется, то с помощью метода type() можно получить исходный тип.
              Библиотека предназначена для парсинга, экземпляр класса живёт одну итерацию цикла, в одной ветке условного оператора, но если вам хочется использовать этот класс глобально или гонять по коду туда сюда, то это ваше дело.


              1. GreedyIvan
                17.11.2019 20:43

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

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


                Это не библиотечный функционал, а хелпер под конкретный, причем типовой, случай.


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


    1. shuchkin
      17.11.2019 16:35

      extract 1) небезопасно, 2) медленно


  1. Vilaine
    18.11.2019 08:28

    Писать проверки вручную утомительно
    Лучше посмотрите в сторону статического анализа кода.


    1. SbWereWolf Автор
      18.11.2019 13:05

      имелось в виду бесконечные:

      key_exists('key',$array);
      !empty(value) ? value : $default_val;
      


      1. Vilaine
        18.11.2019 21:18

        Такие проверки могут понадобиться лишь на стыке слоёв, например, для валидации входящих данных. В этом случае существует множество готовых валидаторов. Если речь о работе с БД, то приведения типов вполне достаточно, так как с БД «жесткий» контракт.
        Типобезопасности в вашем решении я не вижу. Аналогов вашей утилиты нет потому, что непонятно, какую задачу вы решаете.
        Кстати, Psalm поддерживает ассоциативные массивы psalm.dev/docs/annotating_code/type_syntax/array_types/#object-like-arrays — вот с ним можно добиться кое-какой типобезопасности.


        1. SbWereWolf Автор
          18.11.2019 23:44

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

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

          Потом мне надо было получать в API данные и тупо фигачить их в базу, но не в плоскую структуру а в иерархическую, перед тем как значение положить в базу его надо было привести из строки в какой то тип, потом я стал формировать записи БД через методы классов и уже в методы класса-фабрики надо было передавать те же самые значения, и когда ты в аргумент с типом инт отдаёшь переменную с типом стринг, интерпритатор почему то падает.
          И вот как то так пришёл к приведению типов.
          При чём вся эта крутоверть была не на одном проекте а на четырёх разных за полгода, с разными стеками, общее было только в PHP, даже базы были разные, SQLite Postgres MySql, полный привет, особенно что касается работ с датами, везде свой методы.

          Я копипастил класс из проекта в проект, потом сделал первую версию библиотеки, оказалось что устанавливать через композер это очень удобно.
          Потом думал о том что ещё можно добавить в класс, и конечно использовал класс в работе, чего не хватало или что элементарного можно было добавить — добавил.
          Последний месяц уже специально для публикации готовил код. По репозиторию можно понять.
          Да, конечно подход, «всё есть строка» это конечно правильный подход, но не для обработки данных.
          Вообще это всё прелюдия к другой очень большой публикации, про работу с данными, ближе к новому году или сразу после него код дойдёт до нужной кондиции и сделаю релиз.

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


          1. E_STRICT
            19.11.2019 12:23

            Фрактал вроде как раз эти задачи и решает.
            fractal.thephpleague.com


  1. E_STRICT
    18.11.2019 16:12

    !empty(value)? value: $default_val;
    empty() не подходит здесь, потому что для integer, 0 не пустое значение. Тоже самое для false для boolean.
    Проверяйте на null.
    $array['key'] ?? $default_value;