Symfony – один из самых популярных enterprise-фреймворков на PHP. Если вы слышали про MVC, знаете про Request, Response, владеете знаниями ООП и хотите разрабатывать проекты с хорошей архитектурой и легкой поддержкой – смотрите в сторону Symfony. Это сложный, но и обладающий рядом преимуществ фреймворк: продвинутая ORM в виде Doctrine, удобный роутинг, поддержка аннотаций, сотни готовых бандлов по реализации популярного функционала, крутой Security компонент, формы, etc.
Напишем сокращатель ссылок, который не только сокращает, но и ведет статистику по количеству переходов.
Symfony – это MVC фреймворк, но тут есть некоторые замечания. В Symfony нигде явно не указан модельный слой, как это сделано, например, в Laravel, где вся модель (чаще всего) пишется в одном единственном классе. В Symfony Модель – это сущности, сервисы, Value Object и другие компоненты, логику которых вы пишете сами.
Установка и настройка
Как и любой фреймворк или библиотека, Symfony устанавливается через пакетный менеджер Composer так:
my_project – имя будущего проекта: может быть любым.
После установки фреймворка настройте соединение с базой. Поскольку Symfony достаточно дружелюбный PHP фреймворк, создать новую базу вы можете прямо из него, отредактировав .env файл из корня проекта:
Значения db_user, db_password и db_name меняйте на свои, а db_name вообще необязательно должен существовать в системе. Symfony создаст базу сам, когда вы выполните следующую команду в терминале в корне проекта:
Фреймворк Symfony обладает богатым набором консольных команд, сильно облегчающих жизнь разработчика.
Знакомство с фреймворком
Весь код, который вы будете писать, находится в src. Там вы можете видеть папки Controller, Entity, Migrations и Repository. Конечно, это не все папки, которыми нужно ограничиться, и даже не строгая структура, которой нужно придерживаться, но почти достаточно для нашего проекта.
Еще не работали с Symfony? Немного пробежимся по матчасти.
Сущность – это объект, используемый для хранения данных. Другими словами, свойства сущности – это поля в таблице базы данных. Тип, размер и название мы указываем с помощью аннотаций Doctrine ORM. Чтобы создать сущность, мы для начала должны определиться, какие свойства нам нужны. Нам нужен id, верно? Но мы будем использовать не автоинкрементный идентификатор, а UUID. Необязательно, но почему бы не начать его использовать?
Во-первых, UUID обеспечивает почти стопроцентную уникальность записи в пределах всех таблиц вашей базы данных, во-вторых, UUID можно генерировать прямо на клиенте, что в некоторых случаях очень полезно. И, наконец, в-третьих – обеспечивает невозможность просто так спарсить ваш сайт или получить доступ к любой записи вашего публичного API.
Что нам нужно еще? Конечно, URL, который мы сокращаем. URL можно представить в качестве не простого типа данных, а Value Object, что обеспечивает строгую типизацию и валидацию, гарантирующую, что на момент создания сущность всегда валидна. Также нам нужен токен (token), дата создания (createdAt) и количество переходов (views). Кажется, все.
Прежде чем создать сущность, нужно создать объекты-значения (Value Object), без которых сущность существовать не может. Но где их создать?
Поскольку мы собираемся использовать Doctrine, наш объект-значение придется аннотировать как @ORM\Embeddable(), но по умолчанию Doctrine смотрит только в папку App\Entity. Да, можно создать отдельную папку ValueObject и в настройках добавить возможность доктрине видеть и эту папку тоже. Это возможно, но объекты-значения всегда принадлежат конкретной сущности в системе. Именно поэтому их надо класть рядом. Отсюда вывод: создать папку Model, в ней папку Link (это и есть наша модель), куда поместить Entity и ValueObject. Теперь в файле config/packages/doctrine.yaml можем прописать следующие настройки, чтобы доктрина увидела наши сущности и объекты-значения:
Объекты-значения
Value Object – это простой пользовательский тип данных, проверенный на границы и тип. Вы можете захотеть использовать простой строковый тип данных для хранения и передачи URL. Но до попадания в сущность или же уже в ней его нужно проверить на соответствие, что бессмысленно, если вы забыли вызвать проверку, или сама сущность невалидна, с неправильным электронным адресом. Вот как будет выглядеть URL:
Аннотация @ORM\Embeddable() говорит доктрине, что класс – встраиваемый объект, используемый не напрямую, а внутри какой-то сущности. В @ORM\Column(type=”string”, unique=true) определяется тип данных, уникальность, длина и многое другое. Тем, кто знаком с Java или активно на нем пишет, это будет напоминать аннотации из другого популярного фреймворка – Spring.
В конструкторе класса мы проверяем, является ли переданный электронный адрес валидным. Если нет, выкидываем кастомную ошибку. Если да – обрезаем последний слеш, иначе два одинаковых электронных адреса могут восприниматься системой по-разному, если у одного из них будет такой слеш. Дальше делаем простой геттер и имплементацию метода __toString(), чтобы передавать наш объект напрямую без геттера.
Второй объект-значение – токен. Его можно бы оставить обычной строкой, но тогда неизвестно, кто и какой токен нам сгенерирует, а нам нужно оставить одно единственное правило генерации. Вот такой класс Token:
Вы можете выбрать любой другой алгоритм генерации случайного значения, это неважно.
Сущности
Как будет выглядеть наша сущность – уже определились: у нее UUID, URL, token, views и createdAt. Чтобы начать использовать UUID, скачайте библиотеку ramsey/uuid-doctrine следующей командой в терминале в корне проекта:
После в файле config/packages/doctrine.yaml пропишите настройки:
Так мы сообщим Доктрине, что теперь есть специальный тип UuidType, который мы будем использовать в аннотации к полю. Пишем сущность:
Мы сделали свойства публичными, тогда как всегда желательно делать их приватными, предоставляя доступ только через публичные аксессоры. Но поскольку мы используем объекты-значения, а два других свойства инкапсулировали в конструкторе, мы можем быть уверены, что придут значения только нужных типов.
Эта аннотация к полю означает, что наш uuid будет первичным ключом, уникальным, типа uuid, генерировать значения будет кастомный генератор Ramsey\Uuid\Doctrine\UuidGenerator.
Аннотация определяет, что типом данного поля будет встроенный объект Url и его поле value, над которым мы поставили ORM\Column.
Здесь мы указываем, какой репозиторий будет заниматься обработкой сущности.
Также напишем простые геттеры, и все, сущность готова. Осталось обновить базу. Сделаем это:
Когда вы будете обновлять маппинг сущности в следующий раз или создадите новую, вам нужно будет выполнить две другие команды:
Контроллеры
Важный компонент MVC фреймворков – это контроллеры. Их основная задача – обработка запросов пользователей к приложению. Нам понадобится три экшена: для вывода главной страниц с формой, для обработки формы и для редиректа на электронный адрес, который мы сократили.
По умолчанию, фреймворк Symfony предлагает наследоваться от AbstractController, в котором уже есть нужные компоненты: шаблонизатор Twig, роутинг, методы по работе с безопасностью, EntityManager и многое другое. Но чтобы познакомиться с фреймворком поближе, предлагаем не наследоваться вообще, а все, что нам понадобится, инжектить через конструктор. Так мы поступим с нашим репозиторием, менеджером, роутингом и твигом.
Вы могли заметить уже второй раз класс LinkRepository. Это не просто так. Дело в том, что Symfony использует паттерн Репозиторий для управления коллекцией объектов: в репозиториях вы описываете логику по тому, как достать и что достать из коллекции. Если вы используете консольные команды Symfony для быстрого описания и генерации сущностей, репозиторий для этой сущности создается автоматически. Так он выглядит:
Как видите, по умолчанию у каждого репозитория есть несколько основных и часто используемых методов выборки коллекций (они указаны в аннотации к классу).
Пишем простой экшен по выводу формы:
Роутинг так же, как и поля сущностей, может настраиваться через аннотации, где первым аргументом указываете путь на сайте (/ – это главная страница), имя маршрута, которое можно использовать в ссылках тега, методы (POST, GET или другие), а также регулярные выражения для полного соответствия.
Экшены контроллера должны возвращать объект Response. Если бы мы унаследовались от AbstractController, достаточно было бы вернуть $this->render() с именем шаблона. Но раз мы этого не сделали, стоит обернуть вызов шаблона в объект Response и вернуть его.
PHP фреймворк Symfony использует шаблонизатор Twig. Если вы знакомы с Laravel и работали с Blade, с Twig разобраться не составит труда. Вот так выглядит шаблон links.html.twig:
Шаблонизатор Twig позволяет наследовать шаблоны друг от друга, в результате чего нужно всего лишь унаследовать base.html.twig и переопределить его блоки. С помощью функции path в теге action вы указываете, какой роут будет обрабатывать данную форму. Напишем роут:
Экшен принимает объект Request и проверяет, есть ли у него параметр url из формы. Если нет, возвращает пользователя на форму обратно. Создаем объект Url, передавая туда данные из Request. Далее достаем из коллекции объект с таким же электронным адресом: если его нет, создаем объект сущности Link, заполняем в конструкторе и отдаем менеджеру, который сначала подготавливает, а потом обновляет таблицу после вызова flush().
Теперь можем достать сгенерированный токен и отправить его на этот же шаблон с формой. Если такой Link уже есть в базе, сначала достаем из $existsLink токен, который является объектом класса Token, а потом значение, и отправляем на ту же форму.
Теперь нужно обновить шаблон, чтобы он показывал укороченную ссылку:
Если у нас переменная code определена, составляем ссылку с названием нашего сайта (http://localhost:8000/) и токеном. Иначе говоря, после вставки электронного адреса в форму и нажатия на кнопку, сверху формы у вас появится url вида http://localhost:8000/dg3f.
Осталось реализовать последний экшен, который по нажатию на сгенерированную ссылку будет редиректить пользователя на тот электронный адрес, которому соответствует токен.
Роут выглядит как / , где token – это плейсхолдер, то есть он может постоянно меняться. Получаем токен из Request, и если такого такого токена нет (то есть кто-то решил случайно подобрать его руками), генерируем ссылку на любой другой сайт (в нашем случае наш сайт). Иначе делаем редирект на электронный адрес, который достаем из объекта Url, что является свойством класса Link.