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

Теперь, прежде чем идти дальше, я собираюсь очень сильно предостеречь. Массивы PHP сами по себе не являются плохой вещью™. Проблема возникает из-за исторического и, во многих отношениях, продолжающегося злоупотребления сообществом PHP массивами как единственной структурой данных. В языке, который получил достойную поддержку для объектно-ориентированного программирования на сравнительно позднем этапе своего жизненного цикла и мало принимался сообществом в течение многих лет после этого, инстинкт рассматривать массивы как чуть ли не единственный инструмент в наборе инструментов очень понятен. Проблема в том, что времена изменились — практически никто больше не использует PHP4, и даже PHP5, который имеет правильные объекты, подходит к концу. А в наши дни написание или сопровождение приложения, использующего массивы массивов примитивов в качестве основного типа данных, представляет собой упражнение, примерно схожее с выдергиванием зубов безнаркоза.

Я также оставлю короткую заметку для присутствующих в зале теоретиков-пуристов (к которым я сам принадлежу). Хотя я хорошо знаю разницу между массивами и картами, из-за того, как PHP объединяет их в один примитивный тип, я использую термин «массив» исключительно здесь, чтобы избежать какой-либо двусмысленности.

Так как же, черт возьми, мы сюда попали?

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

Короче говоря, до PHP 5 (первоначальный выпуск: 2005 г.) объекты (и объектно-ориентированный код) были, по сути, грубо пристроенными функциями языка. Не было ни интерфейсов, ни способа объявить что-либо как приватное или защищенное, и практически не было более мощных объектно-ориентированных абстракций, которые мы считаем само собой разумеющимися в таких языках, как Hack, Java и C++. Это, в конечном счете, сделало объекты PHP 4 немногим лучше, чем репозитории для процедурного кода, и фреймворки и инструменты той эпохи, как правило, отражали это. Это было верно даже после выпуска PHP 5, поскольку PHP 4 продолжал оказывать доминирующее влияние на то, как сообщество думало и подходило к проблемам (и даже пережило пару второстепенных версий PHP 5).

Но какое отношение все это имеет к массивам, спросите вы? Ответ на самом деле довольно прост — если ваши объекты в основном представляют собой прославленные массивы, вы могли бы также обойтись без накладных расходов и просто написать процедурный код старой школы, используя языковые примитивы в качестве основного типа данных, возможно, используя объекты для какой-то незначительной попытки организации кода. Многие фреймворки (и многие программисты) середины-конца 2000-х использовали этот подход, отчасти вдохновленный Ruby on Rails и его дизайном в стиле ActiveRecord. Среди самых известных из них, конечно же, вездесущий CakePHP, который в первые годы своего существования фактически навязывал использование ассоциативных массивов в качестве основной структуры данных для проектов, использующих его.

Но я все еще не вижу проблемы — что именно не так с массивами?

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

  • Проверка наличия в массиве определенного ключа или ключей с помощью isset(): если вы делаете это, велика вероятность того, что ваш «массив» имеет достаточно сложный набор поведений вокруг него, поэтому он должен быть объект с методами экземпляра.
  • Вложенные ассоциативные массивы. Использование карты допустимо, если вы пытаетесь ссылаться на элементы одного типа по ключам. Но, если у вас есть вложенные массивы в PHP, ваши значения АВТОМАТИЧЕСКИ и БЕЗВОЗВРАТНО больше не относятся к одному и тому же типу, и необходимо затратить массу дополнительных усилий разработчика. по защитному программированию, чтобы убедиться, что они, по крайней мере, достаточно похожи.
  • Параллельные массивы. Это почти неизбежно указывает на то, что вы делаете что-то не так. Если вы передаете в функцию несколько массивов, каждый из которых содержит соответствующие ключи и значения, вам на самом деле следует создать несколько подходящих объектов для управления этими значениями (и поведение, которое на них влияет).

Уже начинаете видеть связь? Проблема с массивами в PHP, в конечном счете, заключается в свободной типизации: невозможность гарантировать, что один элемент массива будет таким же, как и любой другой (по крайней мере, в программном смысле), вынуждая всевозможные махинации защитного программирования, чтобы убедитесь, что вы не получите ключ int, null или несуществующий ключ там, где его действительно не должно быть. В коде, который использует «голые» массивы в качестве основной структуры данных, я часто видел, что 50–75% потока кода «защитны» — т. е. разработчик пытается убедиться, что массив действительно соответствует его представление о том, что оно должно содержать. Весь этот дополнительный проверочный код для того, чтобы сделать то, что может сделать языковая среда выполнения, создает огромные проблемы с ремонтопригодностью и читабельностью кода, независимо от того, насколько хорошо вы его структурируете.

ОК, я вижу вашу точку зрения. Но что я могу с этим поделать?

К счастью, есть несколько способов справиться с этим при написании кода. Однако 0-й шаг является обязательным и простым: прекратите использовать массивы, если только они не используются в качестве коллекций немассивного типа. Для этого обычно для этого используются массивы, и использование их в качестве коллекций массивов, вероятно, как вы попали в этот беспорядок в первую очередь. Очевидно, что из этого есть исключения, но общее правило состоит в том, что вы должны дважды подумать, прежде чем набирать array() или [].

Хорошо, но что дальше?

Хороший вопрос. Первым шагом является осмысление (или, в случае существующих проектов, переоценка) ваших структур данных и способов их взаимодействия с приложением с точки зрения объектов. И нет, не только объекты, где каждое значение общедоступно и может быть изменено каждым методом, через который оно проходит. Вместо этого, как минимум, предоставьте как можно меньше геттеров и сеттеров для поведения, которое должен обеспечить ваш объект. Прежде всего, инкапсуляция — это ключевой момент — предоставление единого определенного типа для заданной структуры данных с определенным набором поведения, которое следует за ним (вместо того, чтобы быть где-то глобально определенным).

После этого пришло время рассмотреть, как на самом деле передаются ваши структуры данных, и где вы можете применить некоторые ограничения на уровне языка. В PHP 5.0 было добавлено то, что тогда было известно как подсказки типов, именно по этой причине — чтобы среда выполнения языка могла сама убедиться, что да, она получает правильный ввод. PHP 7.0 сделал еще один шаг вперед, добавив так называемые подсказки скалярного типа, которые делают то же самое для примитивных типов (например, целых чисел и строк). Итак, что же делать? Просто: вводите подсказки везде. Это не только делает ваш код более самодокументируемым, но и предотвращает появление целых классов ошибок и устраняет большую часть требований к защитному программированию, благодаря самой структуре языка.

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

Аргументы функций с переменным числом аргументов и оператор Splat

Во-первых, если ваша функция должна принимать только одну коллекцию заданного типа, в PHP 5.6 и выше вы можете использовать вариативные (переменной длины) аргументы функции и оператор «splat» (распаковка массива):

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

Пользовательские типы коллекций

Альтернативой являются, конечно же, настраиваемые типы коллекций. Эти классы, как правило, ArrayAccess и предоставляют строго типизированную коллекцию данного подтипа. Затем они, конечно, могут предоставить только тот тип, который они должны содержать, и ничего больше. У этого метода, к сожалению, есть обратная сторона: во многих случаях требуется шаблонный код — по крайней мере, вот простой пример строго типизированной коллекции в PHP:

К этому классу, поскольку он реализует ArrayAccess, можно получить доступ так же, как к массиву. Реализация IteratorAggregate позволит вам без проблем подключить его к циклу foreach(), и вы по очереди получите каждый элемент из $values. Но давайте смотреть правде в глаза, написание пользовательских коллекций — это своего рода головная боль, и хотя я предпочитаю этот шаблон при написании строго типизированного PHP-кода, я все равно не люблю его использовать. Одна вещь, которая может сделать этот тип шаблона менее болезненным, может состоять в том, чтобы абстрагировать его в базовый класс, и я настоятельно рекомендую сделать это, поскольку этот шаблон обеспечивает самые сильные гарантии типов для коллекций.

Генераторы

Существует также, для некоторых случаев использования, третий вариант: генераторы. Начиная с PHP 5.5, в язык включена поддержка таких конструкций:

Теперь генератор не похож на массив. Его можно подключить к foreach(), как массив, но на этом сходство заканчивается. Что такое генератор, в конечном счете, представляет собой один фрейм стека (как вызов функции), но замороженный во времени. Каждый раз, когда извлекается следующее значение, кадр стека извлекается из памяти и продвигается вперед, пока не достигнет следующего выхода. На практике это означает, что генераторы — это PHP-модель ленивых вычислений — вы можете предоставить теоретически бесконечное количество элементов при постоянной стоимости памяти. А поскольку генератор неизменяем после создания, вы можете предоставить гораздо более надежные гарантии того, что он возвращает, не требуя подсказок типа. Большим недостатком генераторов является то, что они не «перематываются» так же, как массивы и пользовательские коллекции — после того, как вы перебрали генератор, он ушел навсегда, и его нельзя повторить снова.

Хорошо, так что же мне делать?

Что ж, это действительно так. Не существует «лучшего способа» справиться с устаревшим приложением, которое повсеместно использует массивы. Прости. Что вы можете сделать, так это по возможности избегать использования массивов массивов и вместо этого рассматривать другие структуры данных. Я представил несколько вариантов, которые сослужили мне хорошую службу, но на самом деле вам и корпоративным повелителям, которым вы служите, решать, какие из них лучше всего подходят для вас и вашего проекта. Удачи!