Всем привет, меня зовут Алексей. Хочу представить вам свой PHPшный фреймворк для создания микросервисов. Он вырос из моего эксперимента трёхлетней давности, который потом перерос в pet project, а позднее на этом фреймворке я создал несколько production проектов.

Когда я начинал его делать, то ставил целью сделать решение, которое:

  • можно легко встраивать в уже существующие проекты;
  • быстро можно создать хоть что-то работающее;
  • максимально лаконичные и выразительные конструкции;
  • разумно использовал возможности современного PHP.

Итак, с чего можно начать? Конечно же с исходников! Посмотреть их можно на github

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

# use mod_rewrite for pretty URL support
RewriteEngine on
RewriteRule ^([a-z0-9A-Z_\/\.\-\@%\ :,]+)/?(.*)$ index.php?r=$1&%{QUERY_STRING} [L]
RewriteRule ^/?(.*)$ index.php?r=index&%{QUERY_STRING} [L]

Далее можно создавать свой первый сервис. В нём сделаем один эндпоинт, который будет обрабатывать метод GET и возвращать сообщение, что у него всё хорошо. Этакий health check.
Для начала нам надо подключить наш фреймворк:

require_once ('vendor/service/service.php');

Потом создаём класс для микросервиса:

class TodoService extends ServiceBase implements ServiceBaseLogicInterface
{ /* class body */ }

Здесь у нас:

  • ServiceBase — это базовый класс сервиса с самым основным и самым утилитарным функционалом;
  • ServiceBaseLogicInterface – это интерфейс, который нужно реализовать любому классу, если он хочет предоставлять обработчики эндпоинтов. Пока этот интерфейс никаких особых требований на ваш класс не налагает. Просто сделан для более строгой типизации.

Потом заводим первый обработчик эндпоинта:

public function action_ping()
{
    return ('I am alive!');
}

После чего запускаем наш первый микросервис:

Service::start('TodoService');

Сложив всё вместе, получим:

/**
 * Service class
*/
class TodoService extends ServiceBase implements ServiceBaseLogicInterface
{
    /**
     * First endpoint
     */
    public function action_ping()
    {
        return ('I am alive!');
    }
}

Service::start('TodoService');

Может возникнуть резонные вопрос – а по какому URL’у доступен этот функционал? Дело в том, что определив метод с префиксом action_<name-part>, вы дали понять сервису, что это обработчик URL’а <name-part> Т.е. в нашем случае это будет что-то вроде localhost/ping
Символы подчёркивания в названии метода меняются на — . Т.е. метод action_hello_world будет доступен по URL’у localhost/hello-world

Погнали дальше.

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

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

class TodoLogic extends ServiceBaseLogic
{
    /**
     * First endpoint
     */
    public function action_ping()
    {
        return ('I am alive!');
    }
}

class TodoService extends ServiceBase
{
}

Service::start('TodoService', 'TodoLogic');

Тут у нас появился класс с логикой:

class TodoLogic extends ServiceBaseLogic

Унаследованные от базового класса ServiceBaseLogic (в нём минимум функций, так что позже подробно рассмотрим его).

Класс TodoService перестал имплементировать интерфейс ServiceBaseLogicInterface (на самом деле он никуда не делся, просто его теперь имплементирует класс ServiceBaseLogic).

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

class TodoLogic extends ServiceBaseLogic
{
    /**
     * First endpoint
     */
    public function action_ping()
    {
        return ('I am alive!');
    }
}

Service::start('ServiceBase', 'TodoLogic');

Здесь уже за старт сервиса отвечает класс ServiceBase а не наш.

Погнали ещё дальше.

В процессе использования своего фреймворка у меня в определённый момент стали получаться классы с логикой монструозного размера. Что с одной стороны раздражало моё чувство прекрасного, с другой стороны Sonar возмущался, с третьей стороны концепцию разделения методов на методы чтения и методы записи (см. CQRS) не понятно было как реализовывать.

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

Т.е. можно либо в рамках одного сервиса сделать всю CRUD логику. А можно разделить на два сервиса:

  • один предоставляет методы чтения;
  • а другой предоставляет методы модификации данных.

Давайте теперь в наш пример добавим метод создания сущности и метод получения списка сущностей:

class TodoSystemLogic extends ServiceBaseLogic
{
    public function action_ping()
    {
        return ('I am alive!');
    }
}

/**
 * Read logic implementation
 */
class TodoReadLogic extends ServiceBaseLogic
{
    public function action_list()
    {
        return ('List!');
    }
}

/**
 * Write logic implementation
 */
class TodoWriteLogic extends ServiceBaseLogic
{
    public function action_create()
    {
        return ('Done!');
    }
}

Service::start('ServiceBase', [
    'TodoSystemLogic',
    'TodoReadLogic',
    'TodoWriteLogic'
]);

Коснёмся только изменений:

  • появились классы TodoSystemLogic (системные методы), TodoReadLogic (методы чтения), TodoWriteLogic (методы записи);
  • при запуске сервиса передаём не один класс с логикой, а несколько.

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

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


  1. PaulZi
    18.12.2019 19:40
    +5

    Стандартный комментарий про тесты, composer и 2019 2020 год.


    1. gdever Автор
      18.12.2019 09:56

      А что не так с тестами? Они есть. По папкам /tests разложены.


  1. AlexLeonov
    18.12.2019 20:01
    +5

    Поставил плюс за старания. Больше пока ставить не за что.

    • Оформите, как пакет composer, укажите все зависимости, в том числе версии PHP
    • Внимательно читайте PSR и применяйте каждый пункт к своему коду
    • Настройте автоматический прогон тестов через travis и повесьте бейджик
    • Узнайте, что такое «автозагрузка» и почему не нужно писать бесконечные require_once


    Я мог бы продолжить этот список и дальше, но на ближайшие месяцы вам хватит.


  1. vlreshet
    18.12.2019 20:11
    +1

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


  1. Psih
    18.12.2019 20:17
    +4

    Привязка к Apache и .htaccess — плохо. На nginx придётся допиливать, т.к. стандартный try_files не отработает из-за необходимости специфичной rewrite rule. Не надо так делать.


  1. SDKiller
    18.12.2019 21:40

    Первым делом нам понадобится .htaccess...

    Дальше даже читать не стал


    1. mikechips
      17.12.2019 23:17

      А что, у вас для Апача есть много других вариантов? Не все пользуются nginx, особенно если речь заходит про шаред-хостинги


      1. t_kanstantsin
        17.12.2019 23:25
        +1

        Сомневаюсь, что фреймворку вообще должно быть дело до nginx/apache


        1. mikechips
          18.12.2019 07:48

          Не должно, но поставлять htaccess — это как бы правило хорошего тона.


      1. SDKiller
        17.12.2019 23:26
        +2

        Микросервисы на шаред хостингах?


        1. index0h
          18.12.2019 00:51
          +1

          на микро шаред хостингах


        1. mikechips
          18.12.2019 07:49

          А почему бы и нет? Модно и дёшево одновременно, в духе экономного бизнеса)


  1. index0h
    18.12.2019 00:50
    +1

    Помимо PSR-ов, composer-ов, да и просто хороших практик (https://github.com/index0h/php-conventions):


    1. Роутинг на основании имен методов — это дико хреновая идея. Что если мне нужен экшн с GET: /user/{userName}/profile/articles/{articleId}/comments/{headCommentId} при этом выражения внутри скобок отвечают неким регуляркам, и еще один экшн POST с таким же путем ?
    2. Юзайте суперглобальные переменные только для создания некого Request объекта в самом начале выполнения.
    3. Ваше разбиение на модули в вендоре не имеет смысла, пример: application не может без конкретной реализации router, хотя router в другом пакете. Причем не приколочен гвоздями через автозагрузку, а наглухо приварен require.
    4. Тут на конце 2019 есть namespace, категорически рекомендую.


    1. vlreshet
      18.12.2019 02:02

      github.com/index0h/php-conventions
      Не вижу форка ниоткуда — это ваш собственный набор?


      1. index0h
        18.12.2019 02:28

        угу


        1. ghost404
          18.12.2019 10:06

          Некоторые рекомендации довольно спорные :(


          1. index0h
            18.12.2019 11:43

            Это правда, я уже молчу про то, сколько времени потрачено в спорах о каждом из них.
            Если у вас есть конструктивные доводы по изменению — пожалуйста укажите их в issues.


            1. ghost404
              19.12.2019 13:14

              Добавил несколько. По возможности, опишу остальное.


    1. ghost404
      18.12.2019 11:39

      раз такое дело, оставлю здесь на почитать про Чистый код
      https://github.com/peter-gribanov/clean-code-php


      1. index0h
        18.12.2019 12:11

        Здорово, спасибо


  1. urands
    18.12.2019 02:07

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


  1. xEpozZ
    18.12.2019 09:55

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