{"id":14268,"url":"\/distributions\/14268\/click?bit=1&hash=1e3309842e8b07895e75261917827295839cd5d4d57d48f0ca524f3f535a7946","title":"\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0442\u044c \u0441\u043e\u0442\u0440\u0443\u0434\u043d\u0438\u043a\u0430\u043c \u0438\u0433\u0440\u0430\u0442\u044c \u043d\u0430 \u0440\u0430\u0431\u043e\u0447\u0435\u043c \u043c\u0435\u0441\u0442\u0435 \u044d\u0444\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u043e?","buttonText":"\u0423\u0437\u043d\u0430\u0442\u044c","imageUuid":"f71e1caf-7964-5525-98be-104bb436cb54"}

Как в PHP из массивов сделать объекты с подсказками?

Когда надоело запоминать ключи массивов и хочется пользоваться подсказками любимого редактора кода на помощь приходит PHPDoc и немного смекалки.

Недавно я очень близко познакомился с TypeScript и познал всю прелесть строгой типизации. Как же это приятно, когда редактор кода подсказывает тебе какие поля есть в объекте и что ты с ними можешь сделать!

В чем проблема?

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

Решение:

Первым делом на ум приходит использовать PHPDoc, но тут появляется проблема:

/** * int[] * array * array<string> */

Нельзя прописать многомерный массив так, чтобы подсказывал редактор кода. Все найденные мной варианты не работают в VSCode и PhpStorm.

Недолго погуглив я нашёл вариант, который заработал только в VSCode.

/** * @var array $arr $arr * @var array $arr['fields'] * @var array $arr['fields']['fieldName'] * @var array $arr['fields']['fieldName']['name'] * @var array $arr['fields']['fieldName']['model'] * @var array $arr['fields'][fieldName]['width'] * @var array $arr['fields'][fieldName]['align'] * @var array $arr['fields'][fieldName]['format'] * @var array $arr['fields'][fieldName]['title'] * @var array $arr['fields'][fieldName]['desc'] * @var array $arr['fields'][fieldName]['readonly'] * @var array $arr['fields'][fieldName]['type'] * @var array $arr['fields'][fieldName]['options'] * @var array $arr['fields'][fieldName]['editor'] * @var array $arr['fields'][fieldName]['default'] **/ $arr = [ 'fields' => [ 'fieldName' => [] ] ];

В таком случае среда подсказывает какой ключ можно выбрать:

Для PhpStorm есть плагин deep-assoc-completion, он требует описания массива в определённом формате:

/** * @var array $arr = [ * 'fields' => [ * $anyKey => [ * 'name' => 'sale', * 'model' => string, * 'width' => '100px', * 'align' => 'center', * 'format' => 'nice', * 'title' => 'Sale', * 'desc' => 'A deal another person that results in money', * 'readonly' => false, * 'type' => 'password', * 'options' => ['option1', 'option2'], * 'editor' => false, * 'default' => '', * ], * ], * ] */ $arr=[ 'fields' => [ '0' => [ 'model' => 2 ] ] ];

В таком случае среда подсказывает ключи:

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

Моё решение:

Редактор кода хорошо индексирует объекты, так почему бы нам не привести массив к объекту и описать его?! Но как описать объект без класса, ведь PHPDoc в этом тоже помощник?

Мы можем создать класс, в котором опишем все нужные нам поля. И вот наш редактор уже подсказывает нам.

Хорошо, а дальше? Многомерные массивы мы можем описать так же как описываем интерфейсы в TS. Либо выносим вложенный массив в отдельный класс:

Либо прописываем в конструкторе текущего класса:

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

Резюме

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

0
23 комментария
Написать комментарий...
Павел Шабалин

Не хотелось вас оскорблять, но битрикс ненавидят как раз из за таких как вы =))
Как вы делаете в типизацию (которая нивелирует огромное количество ошибок еще на моменте интерпритации) с вашими многомерными массивами?

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

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

Ответить
Развернуть ветку
Илья Попов

🤣 А я знаю такой анекдот:
Не зли меня, а то заболеешь.
Чем?
Переломом челюсти и сотрясением мозга.
Будьте осторожны так, как первую уже получили в виде дизлайка.

Ответить
Развернуть ветку
Павел Шабалин

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

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

class Super {}
$myClasss = new Super($myFackerArray);

и всё, одни плюсы =))

Можно фабрику наваять
$myObject = Factory::create(Super::class, $myFackerArray);

Да и вообще в методологиях программирования уже много всего придумано как делать красиво и легко поддерживаемо, просто как правило битриксоиды не хотят расти, они ставят точку вечно =))

Ответить
Развернуть ветку
Максим Бондарь
Автор

Павел, спасибо за мнение. У меня есть опыт работы с D7 и тоже предпочитаю использовать объекты, но когда имеешь дело со штатными компонентами, то вот там как раз в шаблоне мы и получаем многомерные массивы и это печалит.
А костыль да, ещё тот))
Буду признателен, если поделитесь ссылкой на решение с маппером или фабрикой, которое вы считаете более грамотным.

Ответить
Развернуть ветку
Павел Шабалин

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

Ответить
Развернуть ветку
Максим Бондарь
Автор

Согласен про конструктор, немного поправил статью. И я понимаю прекрасно про геттеры и сеттеры, про магические методы я тоже знаю и понимаю. Но нужно будет в конструкторе писать создание экземпляра класса для каждого вложенного объекта.
Вот пример с интерфейсами в TS. Можно вынести объект в отдельный интерфейс и использовать его ещё где-то, а можно оставить объектом с полями. Т.к. такого функционала у php нет, его можно заменить классами и PHPDoc))

Ответить
Развернуть ветку
Dmitry Terta

Не совсем так (и уж совсем не так про сеттеры).
Вы должны создавать нужные вам для работы сущности. И они совсем не должны повторять структуру массива приходящего от Битрикса. Они могут забирать данные из такого массива (а может и из двух, трех и тп). Равно как из одного массива может собираться несколько совершенно разных сущностей.

Собираться это должно, конечно, в конструкторе. Но так как привязывать конструктор сущности к структуре массива Битрикса не очень хорошо, то конструктор должен описывать структуру сущности (особо в современных версиях ПХП) и вообще может быть приватный. А для создания из массива Битрикса стоит использовать отдельный билдер, чаще это именованный конструктор (для ПХП это будет просто статический метод в классе сущности, что позволит основной конструктор делать приватным при необходимости). Если из массивов получается много объектов - конечно сделать отдельно билдер.

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

Ответить
Развернуть ветку
Павел Шабалин

Автор предложил создавать сущности для того чтобы указать их в комментарии.
Остальной поток мыслей ваш вообще не понял если честно =)))

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

2. Про структуру сущности и стурктуру массива чет я поплыл, слишком сложно, а уточнее про новые версии php вообще привели в тупик, к чему вы это? =))

3. Что будет с вашим кодом, если туда передать другой массив с другими данными? Вот у вас есть метод, в нем прописан входной параметр array и? вы уверены что в поле ID там будет число в виде заказа с битрикса а не UUID из 1С? Вам просто надо переложить будет валидацию на сторону разработчика, но для этого завезли типизацию и я буду уверен что в setId(int $id) придет именно ID либо код упадет, логгер его подхватит и я увижу сообщение в телеге и пойду исправлять косячную логику.

4. Про избегать сеттеров можно подробнее? Для чего в модели данных избегать иммутабельности? Можно примером, а то прям интересно стало.

Ответить
Развернуть ветку
Dmitry Terta
Приватный конструктор вам нужен только в единственном случае (по крайней мере в php другого использования я не встречал) это сделать синглтон, чтоб был единый объект для всей системы, но это антипаттерн, в том числе потому что меняет поведение в тестах и может быть изменен из вне.

Приватный конструктор нужен что бы обозначить рекомендованные способы создания объекта. Считайте это моментом документации.

Полезно так же, когда у объекта может быть несколько наборов обязательных полей. Например если $a == "foo" то обязательно $b, а если $a == "bar" то обязательно $c. В едином конструкторе вам придется описать $a и $b и $c, а проверку обязательности унести в рантайм, что всегда хуже, чем проверки на стадии статического анализа. Создаете два именованных конструктора constructForFoo($b) и сonstructForBar($c) - и сразу все видно в момент написания кода. А основной конструктор изолируете от себя же, что бы в будущем не пойти не тем путем. Если понадобится - приватное всегда можно сделать публичным потом.

Про структуру сущности и стурктуру массива чет я поплыл, слишком сложно, а уточнее про новые версии php вообще привели в тупик, к чему вы это? =))

Про версию ПХП - это я про сonstructor property promotion из php 8

Про структуру. Ну вот, условно, у вас из недр битрикса два массива - в одном содержаться данные про автора и посты, в другом - дополнительная информация про автора. Первый массив нужен, что бы создать сущности автора и сущности поста, но первому нужен еще один, второй массив. Те будет Author::constructFromBitrix($bigArray, $additionalArray) и Post::constructFromBitrix($bigArray)

Что будет с вашим кодом, если туда передать другой массив с другими данными?

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

но для этого завезли типизацию и я буду уверен что в setId(int $id) придет именно ID либо код упадет

Где будет рантайм валидация - это уже не так важно. Никто не мешает вам в именованном конструкторе прописать ассерты Assert::uuid($arry['ID']);

Про избегать сеттеров можно подробнее? Для чего в модели данных избегать иммутабельности?

Избегать нужно мутабельности =) Причина простая - иммутабельные объекты предсказуемее. Вы сами частично ответили, когда говорили - почему Синглтон антипаттерн.

Конечно нельзя все делать иммутабельным, но если говорить о сущностях, которые могут изменяться и потом персистится, то "сеттер" тут плохое слово. Потому что должны быть предсказуемые изменения описанные в терминологии предметной области этой сущности. Те самый банальный пример - не setName, а rename. Можно сказать, что это всего лишь как назвать, но на самом деле это о том - как думать об изменении. В целом про сеттеры в моделях написано много всего в интернетах, найти можно.

Ответить
Развернуть ветку
Dmitry Terta

PS конечно про именованные конструкторы - это один из вариантов. Это может быть отдельный класс билдер, который использует основной публичный конструктор. Или класс билдер создающий объект с помощью рефлексии, что часто используется в различных ORM. Это уже вопросы реализации.

Ответить
Развернуть ветку
Илья Попов

Вы, очень интересный человек с хорошим знанием. Мне, всё это очень интересно. 🙂👍

Ответить
Развернуть ветку
Нурлан Марченко

Бро, а огромные, сколько в секунду нагрузка?)

Ответить
Развернуть ветку
Павел Шабалин

Огромных, в плане аудитории =)) Если говорить про нагрузку, то на какую то из черных пятниц я видел до 600 RPS доходящих до битрикса. А так все индивидуально, я видел проекты на 1 500 000 RPS, но тот сервис был очень простой на го и складывал просто запросы в сервис очередей.

Ответить
Развернуть ветку
Boris Zyryanov

1. Есть поддержка описательного синтаксиса массивов в PHPStorm https://blog.jetbrains.com/ru/phpstorm/2021/08/phpstorm-2021-2-release/#array-shapes
2. Поздравляю, вы придумали сериализатор, который даже работает и отвечает вашим требованиям, но результат все равно выглядит странно. Предлагаю рассмотреть возможность использовать готовые библиотеки (https://jmsyst.com/libs/serializer) для решения ваших проблем.

Ответить
Развернуть ветку
Максим Бондарь
Автор

1. Это хорошо, что есть поддержка в PHPStorm, не все им пользуются. Но есть проблема, если вы используете массивы часто и много, а особенно если они большие, ваш код будет изобиловать комментариями, описывающими структуру массива. Возьмите штатный компонент битрикса и несколько шаблонов для него и получится, что в каждом шаблоне вам придётся описывать структуру этих массивов, а если там есть result_modifier.php и component_epilog.php, то ещё по разу в каждом файле. Такое себе решение, как по мне.
2. За совет спасибо, изучу.

Ответить
Развернуть ветку
Boris Zyryanov

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

Ответить
Развернуть ветку
Илья Попов

Я не специалист в этой области и скажу честно, что очень заинтересовала статья.
Спасибо автору за работу.
Только без дизлайка.😊

Ответить
Развернуть ветку
Аккаунт удален

Комментарий недоступен

Ответить
Развернуть ветку
Дешкевич Алексей

Почитайте про phpstan или psalm generics и типизацию и не делайте велосипеды.

Ответить
Развернуть ветку
Максим Бондарь
Автор

Спасибо, я что-то читал, не совсем мне подошло, вроде.

Ответить
Развернуть ветку
Егор Соменко

Посмотрите это:
https://phpstan.org/writing-php-code/phpdoc-types#local-type-aliases
Я не работал с Битриксом, однако если действительно дело завязано на массивах, то можно с помощью статического анализатора указать типизацию в относительно понятном формате.
Еще есть варианты: DTO,ParameterBag,ValueObject,VeilObject. Они более времязатратные, но структурированные.

Ответить
Развернуть ветку
Нурлан Марченко

Мне всегда было интересно .. молодые "специалисты" по Битрикс читали доки по php?))) Складывается ощущение что Битрикс и php это разные миры.
Уважаемый автор, без обид - изучи доки, паттерны + фраймворки, а не прыгай из node в готовый (пусть и -говно) продукт

Ответить
Развернуть ветку
Максим Бондарь
Автор

А по делу будет что-то или жизни будете учить?))

Ответить
Развернуть ветку
20 комментариев
Раскрывать всегда