что такое модуль php
PHP модуль — это просто
Недавно мы опубликовали визард для VisualStudio, с помощью которого можно создать экстеншн в пару кликов мыши. Теперь с помощью него мы напишем наши два первых расширения: «Привет, мир» и «вытащим иконку из exe».
Сразу прошу прощение, что очень сильно задержал статью, но жизненные обстоятельства вынудили это сделать, но они исключительно уважительные.
Итак, начнем.
1. Качаем «волшебника» для VS 2008.
По ссылке из темы VS wizard: PHP extension
Устанавливаем его, это произойдет автоматически.
2. Скачиваем необходимые для сборки файлы.
Нужны лишь исходники PHP и бинарники. Скачиваем 5.2.11 версию обоих файлов
Разархивируем php-5.2.11-Win32.zip в C:\PHPDEV\php-5.2.11-Win32 и php-5.2.11.tar.bz2 в C:\PHPDEV\php-5.2.11.
3. Запускаем VS, создаем новый проект.
И вводим его название. Пути настраивать не придется 😉
После этого видим главное окно студии, смотрим, что же там в файлах.
4. Создаем функции.
Как уже замечено, то скелет полностью создан, осталось лишь написать функции и прописать их.
В проекте есть тестовая функция, раскоментируем ее.
Для справки:
1) Заголовок функции должен быть в h файле. В виде PHP_FUNCTION(имя_функции).
2) Определение — в c файле.
3) Функция должна быть прописана в function_entry test_functions в c файле. В виде PHP_FE(имя_функции, NULL).
Как написать саму функцию, я расскажу позднее. А пока ограничимся этой:
6. Продвинутое создание функций.
Разберемся, как принимать значения из функций и передавать их.
Сложность заключается в том, что функция принимает и возвращает различные значения различных типов.
Рассмотрим пример, при котором принимается строка и целое и возвращается строка.
Как можно увидеть, здесь использованы следующие конструкции:
zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, строка_формата, адреса_дляполучаемых_значений)
RETURN_*;
Рассмотрим 2 таблицы, в первой указаны принимаемые PHP-типы и соответствующие им форматы и типы C.
Во второй возвращаемые значения с соответствующими конструкциями.
Дабы не утруждать себя, я прилагаю фотографии таблиц из книги, которую я всем советую прочитать.
Еще раз смотрим на примеры выше и понимаем, как все просто.
Кстати хочу обратитьт внимание, что выделение памяти ведется через e-аналоги функций c(emalloc, efree, erealloc), это нужно для того, чтобы GC PHP сам смог «прибраться».
7. Полезный пример. Вытащим иконку из exe.
Конечно можно написать это и на PHP, но работы будет больше. А тут уже есть необходимые заголовки.
Напишем код на C(писал одепт bl4de):
В файле pe.h мы видим использование кусков кода из библиотек windows: они нам помогут, а прямое их подключение невозможно, мы ведь пишем кроссплатформенное расширение, не так ли? 😉
В pe.c же пишем код. Как и понятно, мы будем оборачивать функцию void _extract_ico(char *filename, char *filenameOut).
Модульный PHP монолит: рецепт приготовления
Статья написана по мотивам моего доклада на митапе. В нем я рассказываю историю того, как мы взяли и не распилили монолит на микросервисы, и что сделали вместо этого.
На тот момент наша команда работала над приложением, начало которому было положено еще в 2009 году не искушенными в архитектуре студентами. К 2018 это уже был типичный big ball of mud (большой ком грязи), или, этакий «монолит-копролит», как выразился один наш коллега 🙂 Думаю, многим знакомо. Конкретно у нас это вызывало следующие проблемы:
Сложность независимого изменения компонентов приложения, соответственно сложность масштабирования разработки и хрупкость всей системы.
Проблемы с покрытием модульными тестами. Так как наша бизнес-логика зависела от деталей реализации, нам пришлось ввести тестовую базу данных, и модульные тесты перестали быть модульными, к тому же выполнялись долго.
Сложность перехода на другие инструменты, фреймворки, технологии. В 2018 году мы все еще использовали версию фреймворка 2009 года.
Моральная неудовлетворенность разработчиков, работающих с запутанным кодом, тоже не стоит сбрасывать со счетов.
В конце 2018 в корне поменялись бизнес-требования к одной из подсистем приложения, от которой зависели почти все остальные, и нам предстояло полностью ее переписать. Вот тут-то и обнажились все архитектурные проблемы, и это был шанс, чтобы решить их. А возможность параллельной разработки нескольких фич разными подкомандами вообще стало одним из главных требований бизнеса к новой архитектуре.
Вариант с вынесением подсистемы в микросервис(ы) и последующим распилом монолита казался очень заманчивым — попробовать что-то новое, модное, да ещё и решающее задачи бизнеса и проблемы существующего приложения. Тем более, что часть разработчиков уже прошли курсы по golang и просто рвались в бой. Но у нас не было готовой инфраструктуры для микросервисов, не было команды devops и опыта не только проектирования, но и поддержки микросервисной архитектуры. Все это требовало дополнительного времени (а вот его как раз и не было) и ресурсов, а уровень предсказуемости результата был невысок. К тому же очевидно, что и в этой архитектуре можно получить тот же big ball of mud, только распределенный.
Поэтому нам показалось достаточно разумным сначала применить принципы микросервисной архитектуры к монолиту:
провести строгие границы между подсистемами и определить четкие контракты взаимодействия;
упорядочить направления зависимостей;
Тогда если в будущем и возникнет реальная необходимость вынести какую-то из подсистем в микросервис, то это будет сделать гораздо проще. Так родилась идея модульного монолита, которая, конечно же, была не нова.
Итак, что же у нас получилось.
Декомпозиция. Domain-Driven Design.
Для разделения приложения на модули мы использовали принципы и приемы предметно-ориентированного проектирования (Domain-Driven Design).
Domain-Driven Design декларирует, что у каждого приложения есть предметная область. В каждой предметной области можно выделить небольшие подобласти, которые называются ограниченными контекстами. Например, в e-commerce приложении можно выделить следующие ограниченные контексты: customer, product catalog, ordering, shipping. Каждому контексту будет соответствовать свой модуль. Физически он будет представлять собой отдельную папку со своим namespace’ом. Замечу, что БД по-прежнему остаётся общей, но об этом чуть ниже.
Подробнее про Domain-Driven Design можно почитать в книгах Eric Evans «Domain-Driven Design: Tackling Complexity in the Heart of Software» и Vaughn Vernon «Implementing Domain-Driven Design».
Чистая архитектура
Как уже упоминалось, одной из причин проблем нашего монолита была сильная зависимость бизнес-логики от деталей реализации. Для ее устранения мы воспользовались принципами «чистой архитектуры» (Роберт Мартин «Чистая архитектура. Искусство разработки программного обеспечения»), выделив внутри каждого модуля слои: domain, application, infrustructure.
Итак, основные принципы чистой архитектуры:
приложение строится вокруг независимой от других слоев объектной модели;
внутренние слои определяют интерфейсы, внешние слои содержат реализации интерфейсов;
направление зависимостей — от внешних слоев к внутренним;
при пересечении границ слоя данные должны преобразовываться.
Правило зависимостей (Dependency Rule) — ключевое правило. Для достижения такого направления зависимостей нам на помощь придет принцип инверсии зависимостей (dependency inversion). И если в традиционной трехслойной архитектуре бизнес-логика непосредственно зависела от слоя доступа к данным, то в чистой архитектуре она зависит только от интерфейса, который определен в этом же слое, а его реализация находится в слое инфраструктуры. Таким образом бизнес-сервис не зависит от инфраструктуры. В рантайме, конечно, вместо интерфейса будет подставлена конкретная реализация, и этого можно добиться за счет механизма dependency injection, который предоставляют, наверно, все современные фреймворки.
Итак, чистая архитектура дает нам независимость бизнес-слоя от:
Соответственно, разработка модульных тестов сильно упрощается. Дополнительный бонус — ускорение выполнения модульных тестов, так как отсутствуют обращения к физической БД и шаги по ее инициализации тестовыми данными. Также мы получаем относительную простоту замены каких-либо реализаций.
При такой архитектуре структура проекта выглядит следующим образом:
Каждый модуль выделен в отдельную папку, также как и каждый слой внутри модуля выделен в отдельную папку. Есть папка, которую мы назвали Common, там находится библиотечный код, не специфичный для какого либо домена.
Взаимодействие модулей. Anti-corruption Layer.
Еще один шаблон, который мы использовали, это Anti-corruption Layer. Он также впервые был введен Эриком Эвансом в книге «Domain-Driven Design»:
Create an isolating layer to provide clients with functionality in terms of their own domain model. The layer talks to the other system through its existing interface, requiring little or no modification to the other system. Internally, the layer translates in both directions as necessary between the two models.
ACL наших модулей представлен адаптерами, которые обращаются напрямую к API другого модуля, но преобразуют входные данные в модели вызываемого модуля, а выходные данные в модели вызывающего модуля.
Например, есть модуль заказов Ordering и модуль доставки Shipping. Сервис доставки получает информацию о заказах по их ids от модуля заказов. Для этого в модуле Ordering мы выделяем API, который будет являться контрактом для взаимодействия с ним. Сам модуль получает данные из таблиц общей БД, компонует их в DTO и через API отдает модулю Shipping. Если мы будем напрямую использовать это DTO в модуле доставки, мы создадим очень жесткую связь. При изменении контракта модуля заказов нам придется менять все места использования моделей этого модуля в других модулях, чего нам хотелось бы избежать. К тому же в модуле Shipping нужны не все данные заказа, а только некоторые из них, например, информация о клиенте, о его адресах доставки, способе доставки. Поэтому в модуле доставки определяется и используется своя модель заказа. К этой модели адаптер и преобразует полученные данные. И, пожалуй, это самый сильный аргумент в пользу адаптеров, ведь многим разработчикам кажется оверинжинирингом писать адаптеры к собственному коду.
Ниже показан пример организации кода.
В модуле заказов есть папка Api, у нее есть своя модель OrderInfo, ApiInterface и реализация ApiInterface.
В модуле доставки на уровне инфраструктуры размещается папка Adapter. Там находятся классы, которые имеют право обращаться к другим модулям. Любые классы вне этой папки не имеют права взаимодействовать с другими модулями.
Важно учесть: так как у нас общая база данных, то все могут обращаться к ней напрямую. Но это создает жесткую связь по данным. Меняя структуру таблиц заказов, мы должны перепроверить, что остальные модули работают правильно, потому что мы могли их сломать. Чтобы избежать такой сильной зависимости, мы получаем «чужие» данные только через API модуля, напрямую в базу за ними не ходим.
На практике бывает сложно такое реализовать, особенно когда идет выборка для вьюшной модели, и там нужны данные из нескольких модулей. В этих случаях можно использовать CQRS.
Взаимодействие с внешним миром. API Gateways.
Клиентами нашего API могут быть внешние сервисы, мобильные приложения, UI. Для каждого из них мы выделили отдельный API Gateway, который может обращаться и собирать информацию из разных модулей через их API. Так как клиенты могут требовать данные от нескольких модулей, неразумно было размещать API Gateway непосредственно в модулях, поэтому мы их вынесли на верхний уровень.
Итоговая структура проекта
Контроль зависимостей
К сожалению, PHP не предоставляет механизмов контроля зависимостей между нашими модулями и инкапсуляции деталей их реализации. Без них очень сложно контролировать соблюдение принятых принципов, поэтому нужно реализовать этот контроль другими средствами. Мы используем статический анализатор кода deptrac для контроля зависимостей между классами. Он устанавливается через composer и запускается через командную строку. Проверка зависимостей встроена в наш CI/CD, и код, нарушающий архитектуру, не попадет ни в тестовое, ни в прод окружение.
Для настройки утилиты у нас есть два файла: один depfile-layers.yaml, контролирующий зависимости между слоями, и второй depfile-modules.yaml, описывающий зависимости между модулями приложения. В файле настроек зависимостей между слоями мы прописываем названия слоев и правила, по которым в этот слой попадает код:
Domain не может зависеть ни от чего;
Application только от Domain;
API от Domain и Application;
Infrastructure от всех внутренних слоев.
С настройкой зависимостей между модулями было немного сложнее, так как deptrac не поддерживает такое понятие, как «модуль». Поэтому нам пришлось использовать модули как отдельные слои, для которых прописано правило, что они не могут зависеть друг от друга. В исключения добавлена папка Adapter, в которой лежат классы предохранительного слоя, и только через них мы можем взаимодействовать с другими модулями.
Ослабление связей. Event-Driven Design.
Разделив приложение на модули, мы все еще имеем сильную связанность по коду между модулями, так как им постоянно нужно взаимодействовать друг с другом. Рассмотрим ослабление связей на примере онлайн-заказа.
Клиент размещает заказ, он обращается к модулю заказов, модуль заказов обращается к платежному модулю, потом обратно возвращается к модулю заказов, чтобы подтвердить, что платеж прошел; модуль заказов обращается к модулю доставки, чтобы он доставил клиенту заказ. На лицо сильная зависимость модулей друг от друга.
Для решения проблемы мы можем внести в нашу инфраструктуру брокер сообщений и общаться между собой посредством событий.
Наша бизнес-модель может генерировать доменные события. Например, модуль заказов говорит о том, что он создал заказ, и забывает об этом. Событие попадает в очередь сообщений и все, кто заинтересован в этом событии, подписываются на него и производят действия, которые им нужны. В данном случае модуль оплаты требует просто оплатить заказ, потом генерирует свое событие, что заказ оплачен или не оплачен. На него подписан модуль заказов, он его соответствующим образом обрабатывает. С модулем доставки все то же самое.
Таким образом, мы ослабили зависимости тем, что теперь у нас модуль заказов ничего не знает о том, что существуют другие модули. Он просто умеет генерировать события и обрабатывать события других контекстов, которые ему интересны.
Также мы теперь можем легко добавить новый модуль, не затрагивая модуль заказов. Например, нам нужно отправлять клиентам уведомления о событиях (заказ оплачен, подтвержден и т.д.) и для этого у нас появляется модуль уведомлений, который подписывается на интересующие его события.
Важный момент. Для того чтобы участники действительно не были связаны по коду, нужно использовать так называемую weak-schema serialization, то есть простые форматы данных, такие как json, xml. В сериализованных событиях не должно быть никаких названий классов.
Например, класс OrderPlaced
может быть сериализован как json:
Заключение
Итак, подведем итог. Какие приемы мы использовали, чтобы избавиться от big ball of mud:
Разделение приложения на модули с использованием ограниченных контекстов DDD;
Разделение модулей на слои с использованием чистой архитектуры;
Взаимодействие модулей через предохранительный слой;
Ограничение на доступ к таблицам БД;
Ослабление связей с использованием событийно-ориентированной архитектуры;
Эффективная организация кодовой базы;
Контроль архитектуры с помощью deptrac.
Когда подходит такая архитектура?
Нет необходимости в независимом масштабировании отдельных частей приложения;
Нет средств, времени, опыта, знаний для развертывания микросервисной архитектуры;
Для стабилизации границ модулей перед выделением их в микросервисы;
Размеры команды разработки не мешают работать в рамках монолита.
Также можно посмотреть оценку зависимости стоимости разработки от количества и сложности фич, приведенную Sander Mac. Как мы видим, стоимость внедрения микросервисной архитектуры на начальном этапе очень высока. Самая низкая стоимость у монолита. Посередине находится модульный монолит.
Источник изображения: https://twitter.com/Sander_Mak/status/852459340493709313
В заключение хотелось бы привести цитату:
If you can’t build a monolith, what makes you think microservices are the answer?
На нижегородском PHP-митапе 24 апреля 2021 года Валентин Удальцов (ведущий каналов Пых и PHP Point) выступал с докладом «Как структурировать код, чтобы не получить большой ком грязи», в котором предлагает аналогичные подходы к организации монолита.
Разработка модульного движка на PHP
Есть много разных движков на PHP, от достаточно простых, до очень тяжеловесных и громоздих, включающих практически все.
Но на мой взгляд лучший движек — расширяемый. И не такой, что к куче своих возможностей предлагает прикрутить еще и некоторые, а такой, в которого своих возможностей как-бы и нет, а есть только подлкючаемые на выбор.
Меньше полугода назад я задался вопросом создания такого движка. Первым, что нужно было написать, являлся, загрузчик. Эдакий псевдо-модуль (об этом далее), который загружает иные модули.
Давайте определимся со структурой: допустим у нас есть папка mod в которой хранятся модули.
Например /mod/staticpages/*. По стандарту загрузчика модуль должен состоять из конфигурационного файла, главного класса модуля, опционально подключаемых библиотек.
Опять же, например, есть модуль staticpages (который отвечает за обработку статических страниц), он будет состоять из файлов manifest.ini и staticpages.php.
Первый — конфигурация модуля, а второй — файл с главным классом модуля.
Для начала пусть конфиг-файл имеет такую структуру:
При обработке этого модуля открывается его манифест (будем использовать этот термин и далее), из него взымаются необходимые данные, выполняются необходимые операции.
Добавим в манифест еще один отдел:
Теперь при обработке этого модуля мы можем загрузить (точнее включить) файл с его классом и создать его экземпляр.
Сейчас алгоритм будет примерно таким:
1. Читаем все папки /mod/.
2. Если там есть manifest.ini, продолжаем.
3. Получаем значение mainclass, включаем этот файл.
4. Создаем экземпляр класса с именем devname. Именно для этого и нужно это поле.
В итоге мы получаем обьект класса StaticPages с именем $staticpages. Обьект будет являтся глобальным, для удобного взаимодействия иных модулей с ним.
Теперь в дальнейшем коде мы можем просто и быстро использовать возможности этого модуля.
Но теперь мы упираемся еще в одну проблему:
Допустим у нас есть такой запрос: «?ins=staticpage&page=info«, который, по идее, должен показать статическую страницу с именем info. Но как об этом узнает модуль Static Pages, который и должен отвечать за это?
Конечно, можно разместить обработчик в конструктор класса, типа, если ins = staticpage, но ведь на тот момент мы точно не знаем — загрузились ли иные модули, которые нужны для нормальной работы Static Pages, и вообще — стоит ли это делать?
Значит нам нужно добавить в манифест еще один отдел:
Уже после загрузки всех модулей мы запускаем второй этап: запуск методов.
В этой секции, в параметре run указан метод класа, который нужно запустить на этой стадии. Отлично, на этом этапе уже точно будут загружены все модули, и они могут спокойно взаимодействовать друг с другом.
Теперь еще одна ситуация — допустим модуль pex (который отвечает за права доступа) в этой секции только начинает загружать права доступа для текущего пользователя, а нам ну по зарез нужно ограничить показ статической страницы, а вмешиватся в код стороннего модуля совсем не хорошо.
Придется ввести еще одно понятие, а так же параметр в манифест, а именно: очередь и require.
Очередь, в нашем случае, очередь модулей (что очевидно) для выполнения некоторого действия.
А сейчас — модифицируем отдел [Run] модуля Static Pages:
При попытке выполнения метода template() для нашего модуля Static Pages загрузчик наткнется на требование сначала выполнить аналогичное действие для модуля pex, подвинет наш модуль подальше в очереди и продолжит аналогичную работу для остальных модулей.
Теперь мы можем спокойно орудовать данными, получеными pex’ом уже после его Run этапа.
Еще одной важная вещь: проверка жестко установленных модулей для работы нашего Static Pages, а так же их версий. Добавим такой отдел:
Видно, что наш модуль требует загрузчик версии не ниже 0.2 (по этому он и является «псевдо-модулем»: он имеет и манифест, и версию, и к нему можно получить доступ как к любому иному модулю, но он «жестко впаян» в систему), так же модуль для работы с БД MySQL, еще модуль lang (который будет отвечать за кодировку, формат даты и времени, и тд.), а так же модуль для постраничной навигации.
Но наши «очереди» не очень то хорошо сказываются на производительности, да и разработчикам будет утомительно указывать все модули, которые тем или иным боком зависят от него. Поэтому найболее разумным будет сделать «пару уровней» выполнения модульных методов.
Это будут новые отделы в манифесте: сначала будут выполнятся методы с [Prev], уже известный [Run], [After] и [Finish].
Для примера возьмем часть манифеста модуля pex:
Сначала, на этапе [Prev] он получает права группы, к которой принадлежит пользователь, но только после того, как модуль auth получит данные о самом пользователе. Потом он выполнит метод template(), в котором, например, проверит, можно ли текущему пользователю просматривать сайт (но и даст иным модулям сделать свою работу).
А уже после — проверит шаблон на так называемые nodesContainers, участки шаблона, для доступа к которым нужные некоторые права (ведь в пред. этапах разные модули могли добавить такие участки и они бы остались не обоработанные).
Так же не забываем про библиотеки, которые могут понадобится некоторым модулям, добавим еще один отдел:
Загрузчик проверит этот отдел, и при необходимости включит все файлы с папок.
В конце концов в index.php у нас будет примерно следующее:
Что же мы имеем в итоге?
На таком принципе можно построить сайт любой сложности используя уже существующие модули плюс написанные специально. Любой модуль может использовать любой иной, а прямое выполнение кода дает чуть ли не 100% гибкость. Нужен ajax? Создаем модуль, который подождет, пока все будет выполнено и подготовлено, а потом в последний момент отменит показ шаблона и покажет только то, что нужно.
Конечно же в такого способа есть и свои недостатки: из за постояной постройки очередей понижается быстродействие, модули могут лезть совсем не туда, куда нужно, и рядовой пользователь, который столкнется со всем этим, попробует поключить пару модулей, верно бросит все это дело.
Но с начала работы я совсем не располагаю на широкую аудиторию, и все изобратенное используется только для себя. И пока «оно» меня вполне устраивает.
Действующую модель этого способа можно посмотреть на моей страничке (ссылка есть в профиле), а исходные коды — в репрозитории GitHub’а.
Спасибо за внимание, если тема заинтересует кого-то, то напишу топик о переходе от теории к практике.