Привет, Хабр. Недавно получил в руки интересный проект, который, несмотря на свою простоту требовал не использовать какой-либо фреймворк. О пакетах речи не шло, поэтому было принято решение использовать привычные компоненты Laravel. Если есть необходимость в использовании очередей, Eloquent или контейнера — добро пожаловать под кат.


О простоте деления фреймворка на компоненты


Начиная с Laravel 4, все компоненты — отдельные пакеты, которые в теории можно заставить работать в любом PHP проекте. Однако, некоторые компоненты требуют дополнительной настройки, которая в основном фреймворке спрятана под капот.


Компоненты


Контейнер


Из всех рассматриваемых компонентов illuminate/container — самый простой в установке и использовании.


<?php

use Illuminate\Container\Container;

$container = Container::getInstance();

return $container->make(Foo::class);

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


Создадим файл helpers.php для таких хелперов, и добавим его в автозагрузку.


"autoload": {
    "files": [
        "src/helpers.php"
    ],
    "psr-4": {
        "YourApp\\": "src/"
    }
}

Добавляем хелпер app() в файл.


<?php

use Illuminate\Container\Container;

if (! function_exists('app')) {
    /**
     * Get the available container instance.
     *
     * @param  string|null  $abstract
     * @param  array  $parameters
     * @return mixed|\Illuminate\Container\Container
     */
    function app($abstract = null, array $parameters = [])
    {
        if (is_null($abstract)) {
            return Container::getInstance();
        }
        return Container::getInstance()->make($abstract, $parameters);
    }
}

Готово. Можно пробовать использовать хелпер.


<?php

return app(Foo::class);

Query Builder и Eloquent


Для использования компонента illuminate/database мы будем использовать Capsule\Manager — класс, предназначенный для работы с построителем запросов вне Laravel.


Начнём с настройки подключения к БД. Желательно эту настройку проводить на этапе запуска вашего приложения, сразу после подключения autoload.php.


<?php

require_once __DIR__.'/../vendor/autoload.php';

use Illuminate\Database\Capsule\Manager;

$manager = new Manager;

$manager->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'database',
    'username'  => 'root',
    'password'  => 'password',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
]);

// Позволяет использовать статичные вызовы при работе с Capsule.
$manager->setAsGlobal();

Можно начинать работу с построителем запросов.


<?php

use Illuminate\Database\Capsule\Manager;

return Manager::table('orders')->get();

Что насчёт Eloquent?


<?php

namespace YourApp\Models;

use Illuminate\Database\Eloquent\Model;

class Order extends Model {

    protected $fillable = [
        'name',
    ];
}

<?php

use YourApp\Models\Order;

Order::first();

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


<?php

use Illuminate\Database\Capsule\Manager;

Manager::schema()->create('orders', function ($table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->timestamps();
});

Для запуска достаточно выполнить файл с миграцией: php migration.php


Очереди


У очередей тоже есть свой Capsule, однако аналоги queue:work и queue:listen необходимо писать вручную.


Начнём с класса Application. В Laravel этот класс используется как глобальный инстанс контейнера, который помимо методов Illuminate\Container\Container содержит вспомогательные методы для работы с фреймворком (текущая версия, пути к хранилищу, ресурсам). Однако, наш класс будет содержать лишь один метод — isDownForMaintenance. Он необходим для работы воркера, так как определяет состояние работы приложения в данный момент.


<?php

namespace YourApp;

use Illuminate\Container\Container;

class Application extends Container {
    public function isDownForMaintenance()
    {
        return false;
    }
}

Далее, нам необходимо зарегистрировать провайдер illuminate/events, и забиндить контракт Illuminate\Contracts\Events\Dispatcher к алиасу events.


<?php

use YourApp\Application;
use Illuminate\Contracts\Events\Dispatcher;

$application = Application::getInstance();
$application->bind(Dispatcher::class, 'events');

Теперь необходимо создать инстанс Capsule, и добавить туда конфигурации соединений.


Пример конфигурации для БД


<?php

use YourApp\Application;
use Illuminate\Queue\Capsule\Manager;
use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\ConnectionResolver;

$container = Application::getInstance();
$queue = new Manager($container);

$queue->addConnection([
    'driver' => 'database',
    'table' => 'jobs',
    'connection' => 'default',
    'queue' => 'default',
], 'default');

// Также, как и с Illuminate\Database\Capsule\Manager позволяет использовать статичные вызовы для очередей
$queue->setAsGlobal();

$connection = Capsule::schema()->getConnection();
$resolver = new ConnectionResolver(['default' => $connection]);

$queue->getQueueManager()->addConnector('database', function () use ($resolver) {
    return new DatabaseConnector($resolver);
});

Пример конфигурации для Redis (требует установленного illuminate/redis)


<?php

use Illuminate\Redis\RedisManager;
use Illuminate\Queue\Capsule\Manager;

$container->bind('redis', function () use ($container) {
    return new RedisManager($container, 'predis', [
        'default' => [
            'host' => '127.0.0.1',
            'password' => null,
            'port' => 6379,
            'database' => 0,
        ]
    ]);
});

$queue = new Manager($container);
$queue->addConnection([
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => 'default',
], 'default');

$queue->setAsGlobal();

Последний этап в конфигурации — добавление аналога Exception Handler.


<?php

use Illuminate\Contracts\Debug\ExceptionHandler;

class Handler implements ExceptionHandler {

    public function report(Exception $e)
    {
        // Действие, если Exception подпадает под критерии об уведомлении.
        // Пример: отправка в Sentry, отправка сообщения на почту.
    }

    public function render($request, Exception $e)
    {
        // Отображение ошибки
    }

    public function renderForConsole($output, Exception $e)
    {
        // Отображение ошибки, если среда запуска приложения - терминал
    }

    public function shouldReport(\Exception $e)
    {
        // Необходимо ли уведомлять об ошибке
    }
}

app()->bind('exception.handler', function () {
    return new Handler;
});

Конфигурация очередей завершена. Теперь можно приступать к написанию queue:work.


<?php

require_once __DIR__.'/bootstrap/bootstrap.php';

use Illuminate\Queue\Worker;
use Illuminate\Queue\Capsule\Manager;
use Illuminate\Queue\WorkerOptions;

$queueManager = Manager::getQueueManager();

$worker = new Worker($queueManager, app('events'), app('exception.handler'));

$worker->daemon('default', 'default', new WorkerOptions);

Теперь для запуска воркера очередей достаточно написать php worker.php.


Если же есть необходимость в использовании queue:listen, то нужно создавать два отдельных файла. Один — демон, который слушает очередь, и запускает второй файл на каждый новый job. Второй файл, в свою очередь, выступает в роли исполнителя задачи.


worker.php


<?php

require_once __DIR__.'/bootstrap/bootstrap.php';

use Illuminate\Queue\Listener;
use Illuminate\Queue\ListenerOptions;

// По умолчанию, в качестве "второго файла", в Laravel выступает artisan, однако в нашем случае это будет файл work.php.
// https://github.com/laravel/framework/blob/6.x/src/Illuminate/Queue/Listener.php#L72
define('ARTISAN_BINARY', 'work.php');

$worker = app(Listener::class, ['commandPath' => __DIR__]);

$worker->setOutputHandler(function ($type, $line) {
    echo $line;
});

$worker->listen('default', 'default', new ListenerOptions);

work.php


<?php

require_once 'bootstrap/bootstrap.php';

use Illuminate\Queue\Worker;
use Illuminate\Queue\WorkerOptions;
use Illuminate\Queue\Capsule\Manager;

$queueManager = Manager::getQueueManager();

$worker = new Worker($queueManager, app('events'), app('exception.handler'));

$worker->runNextJob('default', 'default', new WorkerOptions);

Переходим к использованию. Все методы можно просмотреть в API


<?php
use Illuminate\Queue\Capsule\Manager;

Manager::push(SomeJob::class);

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


  1. arku
    09.11.2019 19:45
    +1

    Вот в этом репозитории — github.com/mattstauffer/Torch/tree/master/components
    есть быстрый старт для каждого компонента, реально помогает, когда нужно что-то не попсовое, но хочется ларавельное :)


  1. symbix
    09.11.2019 22:09

    несмотря на свою простоту требовал не использовать какой-либо фреймворк. О пакетах речи не шло, поэтому было принято решение использовать привычные компоненты Laravel.

    Прям русская народная сказка "каша из топора". :-)


    Вообще Lumen есть же, я не представляю себе, какие могут быть аргументы против него, если уж все равно illuminate-компоненты используются.


    1. ivan77011 Автор
      09.11.2019 22:15

      Lumen был первым что я предложил взамен целой ларе, однако боязнь «медленной работы» как фреймворков, так и микрофреймворков вылилась в использование пакетов, взятых из этих же фреймворков. Работа была простейшая, поэтому отказываться от неё из-за таких ситуаций не имело смысла.


      1. symbix
        09.11.2019 22:17

        Ну точно каша из топора :-)


      1. Lachezis
        09.11.2019 23:13

        Что такое «медленная работа»? Скорость разработки низкая?


        1. ivan77011 Автор
          09.11.2019 23:30

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


          1. Lachezis
            09.11.2019 23:50

            Как страшный сон вспоминаю время когда за бутстрап нужно было платить. Сразу все вопросы про микро отпадают.


      1. dady_KK
        09.11.2019 23:20

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


        1. ivan77011 Автор
          09.11.2019 23:31

          Второе. Сказано «без фреймворков» — сделано «без фреймворков». Про пакеты речи не шло.


          1. PQR
            10.11.2019 14:16

            Тут вопросы скорее к заказчику, а не к исполнителю — исполнитель всё технично реализовал. Но что за мания у заказчика «без фреймворков»?


  1. be_a_dancer
    10.11.2019 14:12

    Не понимаю позиции заказчика — отказ от фреймворков. Ведь ты в любом случае будешь использовать (микро)фреймворк, только самописный. Убедить не пытались? Если пытались, то как?
    Расскажите, пожалуйста, подробнее.


    1. ivan77011 Автор
      10.11.2019 14:28

      Попытки переубедить не предпринимались, и на то было несколько причин:
      1. Проект сам по себе был простой.
      2. Было желание полазить в ядре лары именно с целью использования компонентов вне фреймворка.
      Если бы проект был посложнее небольшого микросервиса, или у меня было отсутствие желания собирать свой недо-фреймворк, то отказался бы от выполнения заказа.


      1. be_a_dancer
        10.11.2019 14:30

        Спасибо за ответ. А расскажите, почему именно ларовские компоненты использовались, а не симфоневые?


        1. ivan77011 Автор
          10.11.2019 14:34

          Опыт работы есть только с ларой. Компоненты симфы смотрел только если что-то в ларе привело меня к ним (пример)