Из этого урока Вы узнаете как можно быстро создать RESTful API для любого сайта на любой CMS, MODX — это только пример.


Для создания API я буду использовать:



Результат здесь:
https://github.com/andchir/modx2-api


Всё описанное я проделывал на Linux, но я думаю, что на Windows разница в командах будет не большая. Предварительно я установил Composer и утилиту Symfony.


Создание проекта и классов пользователей


Создаю проект:


composer create-project symfony/skeleton modx2-api

Устанавливаю необходимые пакеты для создания API:


cd modx2-api
composer req api
composer req migrations
composer req form
composer req maker --dev

Запускаю локальный сервер.
Если установлена утилита Symfony:


symfony server:start

или так:


php -S 127.0.0.1:8000 -t public

Открываю в браузере адрес для проверки, что всё работает:
http://127.0.0.1:8000/


Открываю файл .env и редактирую строку подключения к базе данных проекта на MODX:


DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name

Теперь нужно создать пользователя и настроить систему авторизации по логину и паролю.


Устанавливаю дополнительные пакеты:


composer req jwt-auth
composer req orm-fixtures --dev
composer req profiler --dev

В файле .env появились новые параметры пакета "jwt-authentication-bundle".


Создаю классы сущности и репозитория (Doctrine ORM) пользователя:


php bin/console make:user

Появились два файла:


src/Entity/User.php
src/Repository/UserRepository.php

Создаю таблицу "user" в базе данных:


bin/console doctrine:schema:create

Настраиваю авторизацию пользователей в соответствии с инструкциями:



Код контроллера App\Controller\SecurityController
<?php

namespace App\Controller;

use App\Entity\User;
use App\Form\Type\LoginType;
use App\Form\Type\UpdateProfileType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="app_login")
     * @param AuthenticationUtils $authenticationUtils
     * @return Response
     */
    public function loginAction(AuthenticationUtils $authenticationUtils): Response
    {
        if ($this->getUser()) {
            return $this->redirectToRoute('api_entrypoint');
        }
        $form = $this->createForm(LoginType::class);
        $error = $authenticationUtils->getLastAuthenticationError();
        $lastUsername = $authenticationUtils->getLastUsername();
        return $this->render('security/login.html.twig', [
            'form' => $form->createView(),
            'last_username' => $lastUsername,
            'error' => $error
        ]);
    }
}

Генерирую класс для создания пользователей:


php bin/console make:fixtures

Подробнее здесь: https://symfony.com/doc/current/security.html#a-create-your-user-class


Код класса App\DataFixtures\UserFixtures
<?php

namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class UserFixtures extends Fixture
{
    private $passwordEncoder;

    public function __construct(UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->passwordEncoder = $passwordEncoder;
    }

    public function load(ObjectManager $manager)
    {
        $user = new User();
        $user
            ->setEmail('admin@admin.com')
            ->setRoles(['ROLE_USER', 'ROLE_ADMIN']);

        $user->setPassword($this->passwordEncoder->encodePassword(
            $user,
            'admin'// пароль
        ));

        $manager->persist($user);
        $manager->flush();
    }
}

Создаю администратора с адресом почты "admin@admin.com" и паролем "admin":


php bin/console doctrine:fixtures:load --append --group=UserFixtures

Позже эти данные можно будет изменить.


Генерирую ключи в папке config/jwt/:


jwt_passhrase=$(grep ''^JWT_PASSPHRASE='' .env | cut -f 2 -d ''='')
echo "$jwt_passhrase" | openssl genpkey -out config/jwt/private.pem -pass stdin -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
echo "$jwt_passhrase" | openssl pkey -in config/jwt/private.pem -passin stdin -out config/jwt/public.pem -pubout
setfacl -R -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt
setfacl -dR -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt

Проверяю создание токена:


curl -X POST -H "Content-Type: application/json" http://localhost:8000/authentication_token -d '{"email":"admin@admin.com","password":"admin"}'

Создаю миграцию:


php bin/console make:migration

Теперь самое интересное.


Генерация API и документации


Генерирую классы сущностей для всех таблиц базы данных:


php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity


Генерирую геттеры и сеттеры для классов:


php bin/console make:entity --regenerate App

Открываю код одного класса, например src/Entity/ModxSiteContent.php. Добавляю аннотацию @ApiResource:


API для ModxSiteContent готово.


Открываю страницу http://localhost:8000/api



Беру токен, нажимаю кнопку "Authorize", вставляю строку с токеном:


Bearer MY_TOKEN


Нажимаю кнопку "Try it out" и затем кнопку "Execute". Получаю такой результат:


Связи таблиц


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


В случае MODX данные пользователей хранятся в отдельной таблице "user_attributes". Например, мне нужно в выборку пользователей по GET запросу добавить их email, имя, телефон и т.д. Открываю код класса App\Entity\ModxUsers, добавляю приватное свойство $attributes и описываю связь с классом App\Entity\ModxUserAttributes в аннотоции "@ORM\OneToOne":


/**
 * @var ModxUserAttributes
 *
 * @ORM\OneToOne(targetEntity="ModxUserAttributes", mappedBy="user")
 * @Groups({"read", "attributes"})
 */
private $attributes;

Снова добавляю недостающие геттеры и сеттеры:


php bin/console make:entity --regenerate App\\Entity\\ModxUsers

Обратите внимание, что я добавил группу "attributes" в аннотацию @ApiResource


Картинка


Открываю код класса App\Entity\ModxUserAttributes, добавляю аннотацию @ApiResource и связь с классом App\Entity\ModxUsers:


/**
 * @var ModxUsers
 *
 * @ORM\OneToOne(targetEntity="ModxUsers", mappedBy="attributes")
 * @ORM\JoinColumn(name="internalKey", referencedColumnName="id")
 */
private $user;

Всем свойствам, которые нужно добавить в выборку, вставляю аннотацию @Groups({"attributes"}).


Картинка


Результат:


В итоге для авторизации в вашем приложении вам нужно сначала отправить логин (email) и пароль на URL "/authentication_token", получить токен и потом этот токен отправлять в каждом запросе в заголовке "Authorization":


Если проект https://github.com/andchir/modx2-api будет интересен пользователям, он будет развиваться. Также жду PR от всех желающих помочь.

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


  1. PerlPower
    09.12.2019 05:19

    Красота! Тот редкий случай когда API обертка над сущностями CMS создается действительно быстро и без костылей.


    1. Dmi3yy
      09.12.2019 09:27

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


  1. pavelgvozdb
    09.12.2019 06:54

    Но ведь это только работа с БД. В MODX есть ещё, к примеру процессоры, которые тоже запускают некоторую логику и перед, и после добавления ресурса. А данное API эти правки опускает, работая напрямую с БД. При чём тогда тут MODX?


    1. Andchir Автор
      09.12.2019 11:35

      В MODX есть ещё, к примеру процессоры, которые тоже запускают некоторую логику и перед, и после добавления ресурса.
      Согласен, есть, например, плагины, которые нужно запускать. Во-первых бывают ситуации когда вам нужно только просматривать информацию, но не добавлять и редактировать. В этом случае такое API потребует значительно меньше времени. Сделать возможность запуска плагинов, думаю, не сложно. Это можно в будущем реализовать. Опять же, это будет быстрее, чем делать с нуля на MODX.


  1. Dmi3yy
    09.12.2019 09:33
    +1

    Не пойму где это применить на практике?


    Если делаем проект на MODX с рестфул то создаем свое апи под проект и нет смысла открывать доступ ко всему.


    Если делаем как базу для рестфул админки то зачем тут Симфони?


    Если уже используем Симфони то зачем нам МОДХ?


    1. Andchir Автор
      09.12.2019 11:25

      Не пойму где это применить на практике?
      Например заказчику нужно мобильное приложение, где он мог бы добавлять страницы, просматривать пользователей или заказы в интернет-магазине. Так же можно использовать для создания облегченной версии админки под конкретный проект. Там некоторый функционал MODX можно не дублировать.
      Если делаем как базу для рестфул админки то зачем тут Симфони?
      Симфони затем, что Api Platform использует именно этот фреймворк.
      Если уже используем Симфони то зачем нам МОДХ?
      Вполне могу представить ситуацию, когда заказчику понравилась CMS MODX (или любая другая), вы сделали сайт, а потом он захотел мобильное приложение. Думаю использовать API Platform будет быстрее, чем писать API на MODX.


      1. Dmi3yy
        09.12.2019 11:43

        На выходе получим ситуацию:


        У клиента был сайт на MODX и он мог всегда найти разработчика любого который знает MODX


        А после такой интеграции ему уже нужен разработчик который знает и MODX и Symfony


        Как по мне такой подход хуже чем написать api в рамках MODX с учетом что задача эта решается пускай и дольше чем через api платформ но не сложнее.


        Вообщем я бы очень осторожно смешивал разные системы в одном проекте ибо потом очень сложно это все поддерживать, говорю на опыте поддержки:


        MODX Revo + Laravel
        MODX Evo + Kohana


        Притом в обеих случаях качество кода было не очень по причине что разработчик кто это делал не мог простые вещи реализовать в рамках MODX и из за этого и прикрутил фреймворк


        1. Andchir Автор
          09.12.2019 11:56

          Да, в данном решении есть свои минусы, но есть и плюсы. Решение нужно выбирать под конкретный проект. Я бы использовал данное решение только для приложений, для которых нужно только чтение (read-only). Таких задач тоже бывает не мало.

          А после такой интеграции ему уже нужен разработчик который знает и MODX и Symfony
          Зависит от сложности приложения, которое будет использовать это API. Если оно простое, то знание Симфони не обязательно, достаточно добавить аннотацию @ApiResource классу и API для таблицы БД готово.


          1. Andchir Автор
            09.12.2019 12:27

            MODX часто используют не программисты, а верстальщики. Они используют стандартный функционал и дополнения. Данное АПИ тоже можно считать дополнением. Если какого-то функционала не хватает, то не программист (или программист, который не работает с Симфони) может добавить свою хотелку (feature-request) github.com/andchir/modx2-api/issues


  1. OnYourLips
    09.12.2019 11:08

    Открываю файл .env и редактирую строку подключения к базе данных проекта на MODX

    Не .env, а .env.local


    1. Andchir Автор
      09.12.2019 12:14

      Можно создать (скопировать) .env.local из .env и указать там параметры подключения к БД и т.п. А можно создать .env из .env.dist. Кому как удобнее. Но ваш вариант более стандартный и предпочтительный, согласен.