Знакомимся с Vesp Core

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



Что я и сделал на github.com/bezumkin/vesp-core, теперь осталось рассказать, как его использовать.

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

1. Мне очень импонирует идея полного (полнейшего!) разделения backend и frontend. Когда на сервере крутится исключительно REST API, а на фронте может быть что угодно, отправляющего ему запросы.

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

С MODX, как вы понимаете, это невозможно. Точнее, API-то вы сможете на нём написать, но при этом всё остальное из ядра вы не выбросите и будете таскать с собой.

2. Использование современной кодовой базы позволяет использовать современные технологии. Если ваше ядро следует соглашениям PSR, то вы можете использовать любые PSR-7 совместимые классы для работы с HTTP запросами, и любые PSR-11 контейнеры для зависимостей. То есть, вам не нужно искать что-то, придуманное именно для MODX или Wordpress, выбор дополнений гораздо шире.

3. Такую систему гораздо проще разрабатывать. Вам не нужно что-то делать в админке и синхронизировать её через Gitify. Вы можете использовать всю мощь подсказок PhpStorm и последних версий PHP.

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

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

Конечно, такой вариант вряд ли подойдёт ребятам, собирающим сайты мышкой в админке, но по мере профессионального роста вы придёте к тем же идеям.

Итак, сегодня я ничего не рассказываю про frontend, потому что Core отвечает за работу API. Давайте попробуем написать свой первый API при помощи Vesp.

Создание проекта

Я провожу все манипуляции на modhost.pro, вы тоже можете создать там тестовый сайт. Дальше заходим через SSH, создаём директорию core, переходим в неё и ставим vesp/core:
mkdir core && cd core
composer require vesp/core
Ждём пока скачаются все зависимости и правим composer.json — нужно добавить в него пространство имён нашего проекта. Должно выглядеть вот так:
{
    "autoload": {
        "psr-4": {
          "App\\": "src/"
        }
    },
    "require": {
        "vesp/core": "^1.1"
    }
}
Ну и создаём директорию src для исходных файлов нашего проекта, там пока будет пусто.

Не забываем сделать composer update.

Настройки

Vesp использует очень популярный способ хранения всех настроек в одном файле с переменными окружения .env. Прелесть его в том, что загружать этот файл умеет не только PHP, но и Javascript, и даже Shell скрипты. По сути, это такое универсальное хранилище настроек для любых программ.

Так что создаём core/.env, куда пишем наши переменные:
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_PREFIX=app_
DB_DATABASE=database
DB_USERNAME=username
DB_PASSWORD=password
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_general_ci
DB_FOREIGN_KEYS=1

Думаю, комментарии излишни — обычные настройки соединения с БД. Запоминать их не нужно — всегда можно подсмотреть в репозитории образец.

Миграции

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

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

В проекте миграции выглядят вот так:


Для работы с миграциями Vesp использует Phinx, который у нас уже установлен, как зависимость, осталось его только настроить.

Так как у нас уже есть файл с настройками, нужно его просто прочитать и вернуть готовое соединение в файле core/phinx.php:
<?php

define('BASE_DIR', dirname(__DIR__) . '/'); // Определяем корень проекта
require BASE_DIR . 'core/vendor/autoload.php'; // Загружаем зависимости
\Vesp\Helpers\Env::loadFile(BASE_DIR . 'core/.env'); // Используем спец.класс для загрузки настроек в память

$eloquent = new \Vesp\Services\Eloquent(); // Запускаем работу с БД (она использует настройки из .env)

return [
    'paths' => [
        'migrations' => BASE_DIR . 'core/db/migrations', // Путь к будущим файлам миграции
        'seeds' => BASE_DIR . 'core/db/seeds', // И к данным по-умолчанию
    ],
    'migration_base_class' => \Vesp\Services\Migration::class, // Базовый родительский класс миграций
    // Может быть несколько вариантов настроек
    'environments' => [
        'default_migration_table' => getenv('DB_PREFIX') . 'migrations', // Таблица с историей миграций
        'default_environment' => 'dev', // Окружение по умолчанию
        'dev' => [
            'name' => getenv('DB_DATABASE'), // БД этого окружения
            'connection' => $eloquent->getConnection()->getPdo(), // И уже готовое соединение Eloquent
        ],
    ],
];
Как видите в этом файле мы уже вовсю используем функцию getenv(), которая получает настройки, загруженные из .env. Эта же логика работы с настройками будет и далее.

Теперь создаём нужные директории:
mkdir db && mkdir ./db/migrations mkdir ./db/seeds

А затем и первую миграцию
./vendor/bin/phinx create Users

У меня вот такой ответ:
Phinx by CakePHP - https://phinx.org.

using config file ./phinx.php
using config parser php
using migration paths
 - /home/s20745/core/db/migrations
using seed paths
 - /home/s20745/core/db/seeds
using migration base class Vesp\Services\Migration
using default template
created db/migrations/20200617094224_users.php

Теперь заходим в наш файл с миграцией и приводим его к такому виду:
<?php

use Illuminate\Database\Schema\Blueprint;
use Vesp\Services\Migration;

class Users extends Migration
{
    // Метод up проводит миграцию и создаёт 2 таблицы
    public function up()
    {
        $this->schema->create(
            'user_roles', // Таблица групп пользователей
            function (Blueprint $table) {
                $table->increments('id'); // Первичный ключ с автоинкрементом
                $table->string('title')->unique(); // Уникальная колонка title
                $table->json('scope'); // JSON колонка с разрешениями группы, их будут требовать контроллеры
                $table->timestamps(); // 2 колонки с датой создания и изменения
            }
        );

        $this->schema->create(
            'users', // Таблица пользователей
            function (Blueprint $table) {
                $table->increments('id'); // Первичный ключ
                $table->string('username')->unique(); // Уникальный логин юзера
                $table->string('password'); // пароль
                $table->integer('role_id')->unsigned()->nullable(); // id группы пользователя, может быть null
                $table->boolean('active')->default(true); // Статус пользователя, выключен или нет. По умолчанию - вкл
                $table->timestamps(); // Даты создания и изменения

                // Внешний ключ колонки role_id
                $table->foreign('role_id') // role_id в таблице users
                    ->references('id')->on('user_roles') // соответствует id в таблице user_roles
                    ->onUpdate('restrict') // При изменении группы ничего не делаем
                    ->onDelete('set null');  // А вот при удалении группы user_id станет null
            }
        );
    }

    // Метод down откатывает миграцию обратно и удаляет 2 таблицы
    public function down()
    {
        $this->schema->drop('users');
        $this->schema->drop('user_roles');
    }
}
Подробнее о синтаксисе работы с таблицами можно прочитать в документации по Eloquent.

Ну а теперь можно создать наши таблички:
./vendor/bin/phinx migrate

Phinx by CakePHP - https://phinx.org.

using config file ./phinx.php
using config parser php
using migration paths
 - /home/s20745/core/db/migrations
using seed paths
 - /home/s20745/core/db/seeds
warning no environment specified, defaulting to: dev
using database s20745
ordering by creation time

 == 20200617094224 Users: migrating
 == 20200617094224 Users: migrated 0.0662s

All Done. Took 0.0842s

И откатить обратно
./vendor/bin/phinx rollback
А затем создать опять, а затем откатить — в общем, пока не надоест. С миграциями всё, переходим к моделям.

Модели

Наши модели должны расширять базовый класс Eloquent Model, и описывать таблицы, которые мы только что создали. Лежать они должны в core/src/Models/.

Но так как мы работаем с Vesp, то можно просто унаследовать модели из неё. Создаём 2 файла:

UserRole.php расширяет Vesp\Models\UserRole:
<?php

namespace App\Models;

class UserRole extends \Vesp\Models\UserRole {}

Ну а User.php расширяет, соответственно, Vesp\Models\User:
<?php

namespace App\Models;

class User extends \Vesp\Models\User {}

Посмотрите на исходники классов, там прописан самый минимум:
— наследуемся от Illuminate\Database\Eloquent\Model, то есть от базовой модели Eloquent.
$fillable указывает, какие колонки можно заполнять через метод fill (типа modX::fromArray())
$casts приводит типы (у MODX это прописывается в схеме)
— и прописаны связи таблиц друг с другом (типа hasOne, getOne в MODX)

По сути, этот функционал есть и в MODX, только для его работы нужно написать XML схему и по ней сгенерировать специальные дополнительные модели для каждого используемого типа БД. А в Eloquent это всё описывается сразу в общей модели, без схем и генераций, что гораздо удобнее и проще.

Подробнее о моделях Eloquent можно прочитать здесь.

Сиды

Это такие файлы, где хранятся начальные данные системы, от английского seed — семячко, которым мы засеем наши таблицы.
Вот нам сейчас нужно как-то создать первую группу и пользователя системы — не вручную же редактировать БД, правда?

Поэтому мы создаём новый сид-файл:
./vendor/bin/phinx seed:create Users

Phinx by CakePHP - https://phinx.org.

using config file ./phinx.php
using config parser php
using migration paths
 - /home/s20745/core/db/migrations
using seed paths
 - /home/s20745/core/db/seeds
using seed base class Phinx\Seed\AbstractSeed
created ./db/seeds/Users.php
В нём будет совершенно обычный PHP код.

Делаем nano ./db/seeds/Users.php и приводим сид к такому виду:
<?php

use Phinx\Seed\AbstractSeed;
use App\Models\UserRole;
use App\Models\User;

class Users extends AbstractSeed
{
    public function run():void
    {      
        // Создаём группу
        $role = new UserRole();
        $role->title = 'Admin';
        $role->scope = ['users']; // Набор разрешений группы
        $role->save();

        // Создаём юзера с указанием группы
        $user = new User();
        $user->role_id = $role->id;
        $user->username = 'admin';
        $user->password = 'admin';
        $user->save();
    }
}
Осталось его только запустить.

./vendor/bin/phinx seed:run

Phinx by CakePHP - https://phinx.org.

using config file ./phinx.php
using config parser php
using migration paths
 - /home/s20745/core/db/migrations
using seed paths
 - /home/s20745/core/db/seeds
warning no environment specified, defaulting to: dev
using database s20745

 == Users: seeding
 == Users: seeded 0.1423s

All Done. Took 0.1435s
Теперь у нас в БД есть новые записи.

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

Поэтому, если вы заметили какую-то проблему или неточность, то можно поменять сид, откатить все таблицы, запустить миграции и засеять их данными:
./vendor/bin/phinx rollback -t 0 && ./vendor/bin/phinx migrate && ./vendor/bin/phinx seed:run
Это удалит все данные и создаст их с нуля. На рабочем сайте таким лучше не заниматься!

Контроллеры

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

При этом контроллеры в Vesp обычно «толстые», то есть они сами делают всё нужное, без вынесения логики в Service Layer. Да, это не модно, но это максимально удобно и понятно людям из мира MODX, как я.

Итак, у Vesp есть 3 абстрактных (предназначенных под расширение) контроллера.

Первый, самый базовый Vesp\Controllers\Controller, в нём только основная логика.
— запрос прилетает в метод process()
— в нём определяется метод (get, post, put, patch, delete)
— затем проверяется, требует ли контроллер какие-то права для работы (свойство $scope)
— если scope требуется, а юзер или не авторизован, или его группа не имеет нужного scope — ошибка доступа
— если проверка проходит, то в контроллере запускается нужный метод (прямо $this->get() или $this->put())
— эти методы получают присланные данные через $this->getProperty('key', 'default_value') или $this->getProperties()
— и возвращают результат работы через методы success($array) и failure('error message')

Собственно, всем остальным контроллерам крайне рекомендуется расширять именно этот базовый, или его наследников. Таких наследников у нас есть еще 2.

Vesp\Controllers\ModelController максимально напоминает class-based processor из MODX:
— от требует указания свойста $model с именем модели
— в нём есть стандартные методы get, put, patch и delete
— а они уже вызывают методы beforeSave, afterSave, beforeDelete, beforeCount, afterCount и т.д.

То есть, это полноценный CRUD контроллер для модели, у которого крайне желательно указывать свойство $scope, иначе любые гости смогут создавать и удалять модели этого ктнтроллера.

Если нужно дать доступ только для чтения, что бывает очень часто на веб-сайтах, то у нас есть последний контроллер Vesp\Controllers\ModelGetController, который расширяет ModelController, и отключет все методы, кроме get.

Для примера делаем простейший контроллер в core/src/Controllers/Web/Users.php
<?php

namespace App\Controllers\Web;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Vesp\Controllers\ModelGetController;

class Users extends ModelGetController
{
    protected $model = User::class; // Указание модели, с которой работает контроллер
    protected $scope = ''; // Никаких прав для доступа не требуется

    // Этот метод вызывается перед получением одного юзера
    protected function beforeGet(Builder $c): Builder
    {
        $c->where('active', true); // Выбираем только активные записи
        $c->with('role'); // Добавляем к ним роль

        return $c;
    }

    // Этот метод вызывается перед получением списка юзеров
    protected function beforeCount(Builder $c): Builder
    {
        $c->where('active', true);

        // Если указан параметр поиска - фильтруем запрос по нему
        if ($query = $this->getProperty('query')) {
            $c->where(
                static function (Builder $c) use ($query) {
                    // Ищем по имени пользователя
                    $c->where('username', 'LIKE', "%{$query}%");
                    // Или по имени связанной с ним группы
                    $c->orWhereHas('role', static function(Builder $c) use ($query) {
                        $c->where('title', 'LIKE', "%{$query}%");
                    });
                }
            );
        }

        return $c;
    }

    // Это вызовется после подсчёта количества результатов списка
    protected function afterCount(Builder $c): Builder
    {
        $c->with('role'); // Тоже добавляем роль юзера к выбираемых записям

        return $c;
    }
}
Как видно из кода, этот контроллеры выдаёт всем желающим список активных пользователей, с возможностью поиска по id и username.

Оцените, какой запрос получается при поиске:
select * from `app_users` where `active` = 1 and (
    `username` LIKE ? or exists (
        select * from `app_user_roles` where `app_users`.`role_id` = `app_user_roles`.`id` and `title` LIKE ?
    )
)
И при этом никакого колдовства с указанием вложенных массив и магических :OR в неочевидной последовательности. Всю работу берёт на себя волшебный Eloquent, главное правильно указать связи между моделями.

Маршруты

Модель и контроллер у нас есть, теперь нужно как-то сделать запрос на веб-сервер для получения списка юзеров.

Для этого нам нужен index.php, который запустит приложение и настроит по каким адресам какие контроллеры будут держать ответ.

Я привожу пример для modhost, поэтому на одном уровне с core (не внутри!) находим или создаём директори www, а в неё пишем index.php:

<?php

define('BASE_DIR', dirname(__DIR__) . '/');
require_once BASE_DIR . 'core/vendor/autoload.php'; // подключение composer
Vesp\Helpers\Env::loadFile(BASE_DIR . 'core/.env'); // загрузка настроек

// Создание Slim4 приложение через мост с контейнером зависимостей PHP-DI
$app = DI\Bridge\Slim\Bridge::create();
// Добавление нужных функций к Slim4
$app->addBodyParsingMiddleware(); // Это оборачивает присланные данные в массивы
$app->addRoutingMiddleware(); // Ну а это, собственно, поддержка маршрутов

// А вот и наш единственный маршрут
$app->any('/web/users[/{id:\d+}]', [App\Controllers\Web\Users::class, 'process']);
// Остальные маршруты можно будет указать здесь же

// Запускаем приложение
try {
    $app->run();
} catch (Throwable $e) {
    // Если будет выброшено исключение - заворачиваем его в JSON
    http_response_code($e->getCode());
    echo json_encode($e->getMessage());
}
Подробности запуска Slim4 с PHP-DI можно прочитать здесь, а документация по самому Slim4 и его посредникам (middleware) тут.

Такая инициализация даёт нам возможность просто указать имя конечнго контроллера в маршруте, и дальше всё запустится волшебным образом. Например, в контроллер автоматически загрузится Eloquent, потому что он указан как зависимость для базового контроллера в его методе __construct().

Теперь по адресу http://site.name/web/users выводится список юзеров, состоящий из одного admin. Вы можете добавить еще ?query=что-то для поиска.

Метод get() может выдавать как список записей для постраничной навигации, так и одну запись, указанную по первичному ключу (обычно, это id записи). Поэтому пробуем сделать вызов /web/users/1, и видим массив с данными нашего юзера.

При получении одной записи вызывается только beforeGet(), а при получении списка будут вызваны beforeCount(), getCount() и afterCount(). Все эти методы нужны, чтобы подлезть в нужный момент к выборке и что-то поменять.

Заключение

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

Всё просто, понятно и прозрачно, с минимальной магией, которую обеспечивают отлично документированные Slim4 и Eloquent.

В Vesp Core, конечно, заложено еще много разных интересных штучек, но мне кажется, что для первого знакомства вполне достаточно.
Если будет интерес, то могу потом еще рассказать про авторизацию через JSON Web Tokens (то есть без сессии на сервере), отладку SQL запросов через Clockwork.
и работу с файлами (загрузка, чтение и т.д.)

У меня на эту библиотеку (или что это — микрофреймворк? минифреймворк? не знаю...) большие планы, и в ближашее время я буду заниматься именно её развитием, потому что она используется во всех моих актуальных проектах, даже в будущей версии modx.pro.

Код из этой заметки я выгрузил в репозиторий github.com/bezumkin/vesp-example, без комментариев и с форматированием по PSR-12, можно брать как образец для экспериментов.

Задавайте вопросы, буду дополнять заметку.
Василий Наумкин
29 июня 2020, 08:03
modx.pro
15
3 192
+32

Комментарии: 31

Алексей Шумаев
29 июня 2020, 14:36
+5
Спасибо, отличный материал!
Конечно, было бы отлично дополнить по JWT, и, особенно, про покрытие тестами.
    Василий Наумкин
    29 июня 2020, 17:02
    +3
    было бы отлично дополнить по JWT
    Сильно зависит от отклика и общего интереса, потому что писать такие заметки весьма трудозатратно.

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


    А тесты для своего будущего приложения нужно будет писать самостоятельно. Хотя, про это тоже могу рассказать, конечно.
      Алексей Шумаев
      29 июня 2020, 19:51
      +2
      Трудозатратно — не то слово, я-то себя не могу заставить/нет времени и документацию нормально написать (

      JWT — тема в принципе уже была описана тобой ранее, а вот тесты — лично мне — очень интересно.
      Больная тема )
    Максим
    29 июня 2020, 15:10
    +1
    Хорошая идея и статья. Я как-то начинал писать подобное с Eloquent, но без Slim и Phinx. Но что-то забросил…
    Поддержу Алексея по поводу JWT и тестов.
    А с чем связан вынос роутов в index.php? Ведь их может быть много и тогда точка входа разрастется… Может их стоит вынести, например, в отдельный сервис? Я не знаю Slim и возможно там это уже реализовано… Ну или в отдельный файлик?
      Василий Наумкин
      29 июня 2020, 17:04
      0
      А с чем связан вынос роутов в index.php?
      Только с тем, что у меня их обычно немного.

      А так, конечно, ничто не мешает их вынести в отдельный файл, да и для тестов это гораздо полезнее.
      Наумов Алексей
      29 июня 2020, 15:19
      +1
      Спасибо за то, что делишься опытом! Интересно ознакомиться.
      Ну и сразу вопрос, а что удобного, красивого, мощного используешь на backend для управления базой данных? Вот чтобы по минимуму кода писать, а быстренько настроить и оно работает, позволяет редактировать объекты?
        Василий Наумкин
        29 июня 2020, 17:07
        0
        Ну и сразу вопрос, а что удобного, красивого, мощного используешь на backend для управления базой данных?
        Это ты про какой-то интерфейс редактирования? Пока что пишу индивидуально для каждого проекта на Vue + Nuxt.


        Но там тоже уже выделяются общие компоненты, которые переходят из проекта в проект, и когда-нибудь они оформятся в подобие админки.
        Артем
        29 июня 2020, 16:56
        +2
        Вот за что люблю твои заметки, так это за то, что они читаются на одном дыхании и всегда до жути интересные, даже если на уже знакомую тему. Знакомую потому, что лично я уже успел прошерстить исходники Vesp Core задолго до заметки. В общем, подача 11/10, как обычно.
        Спасибо за еще одну отличную заметку!

        Конечно, было бы интересно почитать про JWT авторизацию, поскольку она кажется в разы более гибкой и интересной, нежели сессии и вот это все, особенно в контексте полного разделения фронтенда от бэкенда.
          Василий Наумкин
          29 июня 2020, 17:08
          +2
          Конечно, было бы интересно почитать про JWT авторизацию
          Совершенно ничего сложного, в Vesp оно из коробки, нужно только подключить пару контроллеров и один middleware.

          Напишу продолжение, как время будет.
            Артем
            29 июня 2020, 17:36
            +1
            Напишу продолжение, как время будет.
            С нетерпением ждем!

            P.S. Закинул небольшую спасибку
          Alexander V
          29 июня 2020, 20:00
          0
          Походу место пересечение разработчиков в Eloquent. Из Modx большинство идет в сторону Laravel.
            Алексей Шумаев
            29 июня 2020, 20:31
            0
            Есть такое: laravel + vue — фронт, slim4 — бэк/api…
            Eloquent — очень радует.
              Василий Наумкин
              30 июня 2020, 06:46
              0
              Несовсем понятно, зачем использовать Laravel для фронта, если есть Vue и API на Slim4?

              Это же лишнее звено получается, чисто для загрузки javascript.
                Алексей Шумаев
                30 июня 2020, 10:12
                +1
                Это я коряво написал на тему «большинство идет в сторону Laravel».

                У нас modx остался для простых сайтов, более сложное/не типовое сейчас делаем на Laravel (причём выбор Laravel был изначально обусловлен необходимостью научиться работать с современным фреймворком) + vue в классическом виде. Slim4 пока использовали для построения api-приложения.

                Парадигма Vesp Core — дело ближайшего времени, нужно накопить опыт/наработки на vue.
                И тогда, да — наверное Laravel в части проектов станет лишним.

                Eloquent теперь везде )
            Іван Клімчук
            30 июня 2020, 14:42
            +5
            Смотрел, шчупал (как говорит наш пока президент), но остались двоякие чувства. Для старта заготовка хорошая, но есть пару моментов, которые смущают. Никогда не поздно отправить PR, но сначала подсвечу это тут, чтобы понимать, не нарушает ли это концепцию в целом.

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

            Второй пункт просто вопрос. Классика миграций — это up/down, но пробовал ли change у phinx? Пишут, что он умеет сам определять, что нужно откатить. И тут в довесок брюзжание на тему, что в одной миграции две таблицы описаны, а лучше бы разделять сущности (имхо).

            По толстым контроллерам и нарушением правил по service layer в целом понятно и принято, сделать близко к логике MODX, хоть и не канонично :) Но главное работает.

            По github.com/bezumkin/vesp-example кстати, можно поставить в настройках репозитория галочку Шаблон, будет удобно сразу себе в репу утащить и потом просто клонировать уже готовый код, чтобы не возиться с созданием, клонированием, прописыванием remote и тд.

            В остальном неплохой инструмент получился, лайк!
              Василий Наумкин
              30 июня 2020, 14:57
              +3
              Первое, что меня смутило, это метод process, который нужно явно определять в тех же роутах, вместо `__invoke()`, при использовании которого можно передать просто имя класса.
              Во-первых, это тоже из MODX, основной метод процессора.
              Во-вторых, я просто не знал, что можно как-то иначе.

              После того, как я перестал завязываться на мир MODX, каждый день открываю что-то новое в PHP. Там на Git уже 2.x версия в разработке, так что попробую поменять на __invoke().

              Классика миграций — это up/down, но пробовал ли change у phinx? Пишут, что он умеет сам определять, что нужно откатить.
              Насколько я понял, оно работает только с родной схемой CakePHP, а я её не использую. По факту, Phinx здесь только как интерфейс для запуска скриптов. Да и up/down как-то понятнее.

              можно поставить в настройках репозитория галочку Шаблон
              Поставил, спасибо!

              В остальном неплохой инструмент получился, лайк!
              Спасибо, буду продолжать в том же духе.
              Іван Клімчук
              02 июля 2020, 02:27
              +1
              Кстати, тут еще вспомнил, что когда колупал различные скелеты и мануалы по Slim 4, встретил интересный паттерн ADR (Action, Domain, Responder). Это из теорий о гексагональной архитектуре, паттерн сам по себе достаточно спорный и многословный, хотя архитектурно хорош и сделан специально для backend сервисов.
              Хотел узнать твое мнение на этот счет.
                Василий Наумкин
                02 июля 2020, 06:55
                +5
                Хотел узнать твое мнение на этот счет.
                У меня его нет, потому что я просто пишу код, как мне нравится, не задумываясь о шаблонах.
                Dmitry P.
                02 июля 2020, 12:13
                0
                Отличная статья! Ковыряю vesp еще с первых статей, очень много нового для себя узнал.
                Очень интересно было бы почитать еще про работу с файлами и организации, например, галерей.
                  Alexander V
                  04 июля 2020, 07:45
                  0
                  Дело хорошее. Еще бы документацию. Я понимаю, что это занимает столько же времени. Начинающим программистам было бы очень полезно.
                    Sergey Leleko
                    08 июля 2020, 21:20
                    0
                    Сильно помогает во многих вопросах с бэком документация к Eloquent ссылку на которую Василий в статье дал.
                    А с фронтом, довольно мощное комьюнити по nuxtjs когда возникает какой-то не решаемый вопрос (Особенно канал в телеграме).
                    Ну и непрерывное штудирование гитхаба ни кто не отменял ))
                    Sergey Leleko
                    08 июля 2020, 21:17
                    0
                    Совершенно шикарный обзор, спасибо @Василий Наумкин. Последние пол года лично я погружен с головой в изучение и применение VESP на практике. Чего и всем желаю.
                      Иван Бондаренко
                      25 июля 2020, 14:03
                      0
                      Василий, привет! Спасибо за Vesp Core, реально интересно. Помоги советом, пожалуйста. Пытаюсь самостоятельно сделать функционал логина по токенам и зашел в тупик.

                      Прописал путь для АПИ security/login и user/profile в файле index.php
                      Соответственно добавил контроллеры в папку Controllers.
                      Установил модуль авторизации auth. Прописал пути в конгфиге

                      auth: {
                          redirect: {
                            home: '/',
                            login: '/admin/',
                            logout: '/',
                          },
                          resetOnError: true,
                          strategies: {
                            local: {
                              endpoints: {
                                login: {url: 'security/login', method: 'post', propertyName: 'token'},
                                logout: {url: 'security/logout', method: 'post'},
                                user: {url: 'user/profile', method: 'get', propertyName: 'user'},
                              },
                            },
                          },
                        },

                      При отправке формы отрабатывает security/login, токен добавляется в базу. Но затем user/profile возвращает ошибку
                      GET http://dev.website.com/api/user/profile 401 (Unauthorized)

                      Что только я не пробовал. Не получается победить. Вот содержание файла Profile.php.

                      <?php
                      
                      namespace App\Controllers\User;
                      
                      use Psr\Http\Message\ResponseInterface;
                      use Vesp\Controllers\Controller;
                      
                      class Profile extends Controller
                      {
                          /**
                           * @return ResponseInterface
                           */
                          public function get()
                          {
                              if ($this->user) {
                                  $data = $this->user->toArray();
                                  $data += ['scope' => $this->user->role->scope];
                      
                                  return $this->success(['user' => $data]);
                              }
                      
                              return $this->failure('Authentication required', 401);
                          }
                      
                          /**
                           * @return ResponseInterface
                           */
                          public function patch()
                          {
                              if ($password = trim($this->getProperty('password'))) {
                                  $this->user->password = $password;
                              }
                              $this->user->save();
                      
                              return $this->get();
                          }
                      }
                      Можешь подсказать, в чем ошибка? Если что, бекенд сайта лежит на работающем сервере. Фронтенд на моем компе.
                        Василий Наумкин
                        26 июля 2020, 05:15
                        +1
                        А откуда возьмётся $this->user?

                        Нужно подключить еще авторизационный middleware, чтобы он загружал юзера, если увидит токен в куках или заголовках.
                        $app->add(App\Middlewares\Auth::class);
                          Иван Бондаренко
                          26 июля 2020, 09:32
                          +1
                          Спасибо, что подсказал. Заработало! Пока еще не совсем понимаю всю логику, и что и откуда подтягивается. Но упорно разбираюсь.
                        Иван Бондаренко
                        17 августа 2020, 19:02
                        0
                        Василий, подскажи пожалуйста. Уже не знаю, куда копать. Никак не могу получить файл-картинку, отправленную с фронтенда. Целый день сегодня ковыряюсь.

                        Vesp-core версия 2
                        Так прописываю маршрут к контроллеру в index.php:

                        $app->any('/api/user/profilepicture', [App\Controllers\User\Profilepicture::class, '__invoke']);
                        Вот сам контроллер. Подключал в него все, что нашел
                        <?php
                        
                        namespace App\Controllers\User;
                        
                        use App\Models\User;
                        
                        use Psr\Http\Message\ResponseInterface;
                        use Psr\Http\Message\RequestInterface;
                        use Vesp\Controllers\Controller;
                        use Psr\Http\Message\UploadedFileInterface;
                        
                        use Slim\Psr7\UploadedFile;
                        use Slim\Psr7\Request;
                        
                        class Profilepicture extends Controller
                        {
                        	
                            public function patch()
                            {
                                
                        		$files = $this->request->getUploadedFiles();
                        				
                        		return $this->success($files);
                            }
                        }
                        Форма отправляется вот так:

                        const fd = new FormData();
                                      fd.append("firstName", "John");
                                      fd.append('file', this.form.file, this.form.file.name);
                                      console.log(this.form); //тут вижу в консоли, что файл привязался file: File
                                      
                                       const {data: user} = await this.$axios.patch('user/profilepicture', fd, 
                                      { headers: {'Content-Type': 'multipart/form-data' },
                                        onUploadProgress: function(progressEvent) {
                                          var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
                                          console.log(percentCompleted)
                                        }
                                      });
                        Процент загрузки картинки отрабатывает и когда загружаю большое изображение, процесс идет.

                        Но $files = $this->request->getUploadedFiles(); возвращает пустой массив. $this->request->getServerParams(); при этом отрабатывает корректно. Я вижу в ответе, что запрос не пустой

                        "CONTENT_LENGTH": "8579020", // Вот картинка
                            "CONTENT_TYPE": "multipart/form-data; boundary=----WebKitFormBoundary640lESYKvPBsYyvc",
                            "REQUEST_METHOD": "PATCH",
                        Где-то я туплю, но где, понять не могу.

                        Дай совет, что не так. Если есть рабочий пример с реализацией подобного функционала, кинь ссылку сюда или в VESP чат если не сложно. Буду благодарен.
                          Василий Наумкин
                          18 августа 2020, 05:45
                          +1
                          Подозреваю, что это из-за метода PATCH, нужно поменять на POST.

                          Ты же отправляешь классическим старым способом, как форму, только через ajax. Всякие PUT и PATCH могут отправлять файлы только строкой в виде base64.
                            Иван Бондаренко
                            18 августа 2020, 13:10
                            0
                            Спасибо за совет!!! Действительно в этом была проблема! Не знал про такой нюанс.

                            Получается, что по-современному было бы правильнее преобразовать картинку в base64 и отсылать как строку?
                        Артём Кузнецов
                        25 октября 2023, 18:24
                        0
                        Василий ссылка просрочена github.com/bezumkin/vesp-example
                        Скажите пожалуйста планируете ли вы написать доку для этого минифреймворка?
                          Николай Савин
                          25 октября 2023, 18:56
                          +1
                          С Василием, по поводу VESP нужно разговаривать на его профильном форуме, где он отвечает на подобные вопросы. bezumkin.ru/
                          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
                          31