Сегодня зафиналим наш autoselect-компонент. Но для начала расскажу, где я был: дорабатывал положенные 2 недели на старой работе и готовился к выходу на новую. Больше пока сказать не могу, кроме того, что компания большая, требуют там больше и работать надо будет упорно, чтобы пройти испытательный. Зато буду больше писать о всяком. А теперь к нашим баранам. В первой части мы немного затронули Vue, написали первый компонент и закончили на этом. Сегодня так просто отделаться не получится — будет и бэк, и фронт, и интересные либы и много нового материала.
Для начала нужно мОкнуть данные с бэка. Что такое мокнуть? Это значит подделать (получить фэйковые данные для тестов). С этими данными будет работать наш компонент, написанный в 1-ой части.
Чтобы это сделать, нам нужно некоторое количество записей в какой-либо таблице. Я создам таблицу Product, каждая запись в которой будет состоять из 4-х частей: id, название продукта, дата поступления и количество.
Чтобы всё сделать по науке мы познакомимся такой вещью, как фикстуры — классы, которые автоматизируют создание записей и используется для тестов. Я познакомился с этим понятием относительно недавно в одном из манов от symfonycast. Но обо всём по порядку. Сначала установим свежую копию Symfony. Устанавливать буду пакет symfony/skeleton — без дополнительных библиотек. Как это сделать я описывал в одном из первых уроков на канале, там всё максимально подробно и с картинками 🙂 Привожу только листинг:
composer create-project symfony/skeleton autoselect-backend
composer require symfony/web-server-bundle --dev
Первая команда накатит Symfony, вторая — dev-сервер.
Дальше — установим и настроим базу данных. В качестве БД возьмём mysql. Установка:
sudo apt install mysql-server
sudo mysql_secure_installation
Вторая команда запросит ввести пароль для root-пользователя. Тут на вкус и цвет. Теперь создадим пользователя, базу и дадим этому пользователю права на эту базу:
sudo mysql -u root -p
create database autoselect-db;
create user 'autoselect-user'@'localhost' identified by 'test1234';
grant all privileges on autoselect_db.* to 'autoselect-user'@'localhost';
flush privileges;
quit;
Соответственно, база называется ‘autoselect-db’, пользователь — ‘autoselect-user’, пароль для него — ‘test1234’. Проверим, что права этому пользователю выданы корректно и он видит базу:
mysql -u autoselect-user -p
show databases;
quit;

Теперь поставим пакеты для symfony, чтобы играться с базой через ORM:
composer require symfony/orm-pack
composer require --dev symfony/maker-bundle
Откроем файл .env и настроим подключение:
DATABASE_URL=mysql://autoselect-user:test1234@127.0.0.1:3306/autoselect_db
Чтобы протестить, посмотрим, что скажет symfony на запрос корректности схемы данных:
bin/console d:s:v
Лично у меня появилась ошибка, что отсутствует драйвер подключения к БД. Всё верно, забыли накатить адаптер php к mysql. Исправим:
sudo apt install php-mysql
Ещё раз:
bin/console d:s:v

Итак, у нас есть проект, есть подключенная к нему база. Давай создадим сущность Product с уже упомянутыми выше полями:
- id
- название
- дата поступления
- количество
Чтобы создать сущность через консольку — нужно установить ‘maker-bundle’ — бандл, через который можно удобно создавать команды, контроллеры, сущности и всякое:
composer require symfony/maker-bundle
Теперь можем создать сущность Product:
bin/console make:entity
Дальше вводим название и перечисляем поля.
Чтобы модель нашего проекта синхронизировалась с базой, нужно обновить схему:
bin/console d:s:u --force
Теперь напишем фикстуру, которая будет проводить махинации с базой. Фикстура, как я уже сказал выше, этот некоторый метод, заполняющий базу фэйковыми данными. За подробностями можешь заглянуть в документацию. А мы тем временем установим пакет, чтобы начать пользоваться фикстурами:
composer require --dev orm-fixtures
Далее, через только что установленный maker-bundle создадим фикстуру:
bin/console make:fixture

Посмотрим, что получилось:
<?php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class ProductFixture extends Fixture
{
public function load(ObjectManager $manager)
{
// $product = new Product();
// $manager->persist($product);
$manager->flush();
}
}
Метод load будет вызываться при старте фикстуры. В нём создадим обычным циклом 20 объектов, которые заполним рандомными данными. Чтобы было интересней, я установлю и использую пакет, который был использован в мане от symfonycast — faker. Он умеет много чего, но мы его заюзаем для генерации красивых рандомных имён (абсолютно бесполезно, но чертовски весело):
composer require fzaninotto/faker --dev
Инициализируем:
protected $faker;
public function __construct()
{
$this->faker = Factory::create();
}
И теперь определяем метод load:
public function load(ObjectManager $manager)
{
for ($i = 0; $i < 20; $i++) {
$product = new Product();
$product->setName($this->faker->company)
->setAddDate($this->faker->dateTime)
->setCount(rand(10, 100));
$manager->persist($product);
try {
$manager->flush();
} catch (\Exception $exception) {
echo $exception->getMessage() . PHP_EOL;
}
}
}
Далее вызовем фикстуру и посмотрим, что появится в базе:
php bin/console doctrine:fixtures:load

Теперь давай откроем базу прямо в PhpStorm и посмотрим, что получилось. Нажимаем на Database -> «+» -> Data Source -> Mysql. Вводим логин, пароль и название базы. База появится в списке, и дальше может быть 2 варианта: либо как у меня, либо по-человечески 🙂 Если второй — всё будет корректно и можно сразу посмотреть, что находится в таблице Product. Если как у меня, то нужно нажать правой клавишей по базе, затем Database Tools -> Manage Shown Schemas.. -> поставить галочку на схему с названием нашей БД/
Теперь опишем небольшой контроллер и создадим метод в репозитории, который будет возвращать из базы выборку данных по переданному фильтру. Так как мы создавали сущность через make-команду, репозиторий создался автоматически. А вот контроллер нужно создать:
bin/console make:controller
Начнём с репозитория. Переходим в него и описываем новый метод:
/**
* @return Product[] Returns an array of Product objects
*/
public function findByFilter(string $filter)
{
return $this->createQueryBuilder('p')
->andWhere('p.name like :filter')
->setParameter('filter', '%' . $filter . '%')
->orderBy('p.name', 'ASC')
->getQuery()
->getArrayResult();
}
‘like’ в mysql берёт соответствие поля слева к переменной справа. в setParameter мы указали символ ‘%’ с двух сторон от переданной переменной — это значит, что like будет искать слово, которое совпадает с переданным фильтром и можем иметь дополнительные символы справа и слева от фильтра. Например, в таблице у нас 5 строк:
- 11aoa11
- 12aoa12
- bob
- jon
- 11a11
В качестве фильтра мы передали комбинацию ‘aoa’. В ответ метод вернёт 1 и 2-ую строку, так как в них найден фильтр. Всё просто. Теперь переходим в созданный класс с контроллерами и описываем новый контроллер:
/**
* @Route("/product", name="product", methods="GET")
*/
public function getProducts(Request $request, ProductRepository $productRepository)
{
$filter = $request->query->get('filter');
$products = $productRepository->findByFilter($filter);
return $this->json([
'products' => $products,
]);
}
Теперь нужно проверить, что в итоге вернётся. Для тестовых запросов как нельзя лучше подходит такая программа, как Postman. Как скачивать и устанавливать описывать не буду — всё описано на официальном сайте 🙂 Запускаем сервер:
bin/console server:start
И открываем Postman. Добавляем новый запрос, вводим точку входа, на которую хотим ударить и указываем значение фильтра. Тип запроса — GET:

Интересно.. Давай проверим, правда ли в базе только одно название, где есть комбинация буква «na»?

Либо я плохо смотрю, либо и правда фикстура сгенерировала только одну такую строку. А вот комбинацию букв «co» я вижу в нескольких строках. Давай посмотрим, что вернёт запрос с таким фильтром:
http://127.0.0.1:8000/product?filter=co

Воу, целых 3 объекта! Ну, я думаю, что данные мы успешно мокнули и подготовили бэк к взаимодействию с фронтом.
Теперь вернёмся к тому, что мы писали в первой части на Vue. Переходим в папку с проектом и запускаем сервер:
npm run server
Иии.. в браузере открылась наша страничка:

В прошлый раз код компонента выглядел так:
<template>
<div class="autocomplete">
<input
type="text"
v-model="filter"
@keypress="filterOnKeyPress"
>
</div>
</template>
<script>
export default {
data: function() {
return {
filter: null
}
},
methods: {
filterOnKeyPress() {
// eslint-disable-next-line no-console
console.log(this.filter);
}
}
}
</script>
Теперь нам нужно сделать так, чтобы значение, введённое в текстовое поле передавалось на сервер в виде фильтра в ту точку входа (читай контроллер), которую мы создали выше. Ответ мы будем парсить и выводить списком под текстовым полем.
Есть несколько библиотек, которые позволяют отправлять запросы с клиентской части на серверную. Я не буду придумывать велосипед и почти целиком возьму пример из этой статьи документации — запросы будем отправлять с помощью библиотеки axios, а функцию обернём в метод debounce из библиотеки lodash. debounce создаёт ссылку на обёрнутый метод и устанавливает интервал обращений к нему. Допустим, мы обернули функцию, выводящую сообщение в консоль и установили интервал в 1 секунду. В этом случае, даже если мы будем вызывать функцию 10 раз за секунду, она сработает 1 раз — по интервалу. Для нашего кейса это крайне удобно, чтобы не отсылать запросы на каждое нажатие клавиш и поднимать кучу копий php-fpm на каждый запрос. Устанавливаем axios и lodash:
npm install axios lodash
Теперь импортируем их в компонент и пишем логику запроса:
<template>
<div class="autocomplete">
<input
type="text"
v-model="filter"
@keypress="filterOnKeyPress"
>
</div>
</template>
<script>
import axios from "axios";
import _ from "lodash";
export default {
created: function () {
this.debouncedGetProductByFilter = _.debounce(this.getProductByFilter, 500)
},
data: function () {
return {
filter: null,
products: [],
error: null
}
},
methods: {
filterOnKeyPress: async function () {
await this.debouncedGetProductByFilter();
// eslint-disable-next-line no-console
console.log(this.products);
},
getProductByFilter: async function () {
axios.get(`http://127.0.0.1:8000/product?filter=${this.filter}`)
.then((response) => {
this.products = response.data.products;
})
.catch((error) => {
this.error = 'Ошибка! Не могу связаться с API. ' + error
})
}
},
}
</script>
И мы успешно ловим ошибку CORS’а. Что такое CORS — тема для отдельной статьи. Сейчас можно его описать как некая минимальная прослойка защиты нашего бэка, не позволяющая левым запросам с подозрительными заголовками стучаться на наш бэк. Чтобы это исправить — нужно изменить немного настроить конфиги приложения. Но сначала надо установить пакет, который умеет разбираться с cors-политикой:
composer require cors
После чего в конфигах появится новый — nelmio_cors.yaml:

Открываем и изменяем содержимое на следующее:
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['*']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['*']
max_age: 3600
paths:
'^/': ~
Если очень грубо — даём разрешение на любые заголовки с любого домена.
Теперь снова переходим в браузер и пробуем вбить какой-либо фильтр:

Ответ получен, аллилуйя. Осталось всего ничего — распарсить и вывести его списком под текстовым полем и не забыть обработать ошибку:
<template>
<div class="autocomplete">
<input
type="text"
v-model="filter"
@keypress="filterOnKeyPress"
>
<template v-if="null!==error">
{{error}}
</template>
<template v-else>
<ul>
<li v-for="product in products">
{{product.name}}
</li>
</ul>
</template>
</div>
</template>
<script>
import axios from "axios";
import _ from "lodash";
export default {
created: function () {
this.debouncedGetProductByFilter = _.debounce(this.getProductByFilter, 500)
},
data: function () {
return {
filter: null,
products: [],
error: null
}
},
methods: {
filterOnKeyPress: async function () {
await this.debouncedGetProductByFilter();
// eslint-disable-next-line no-console
},
getProductByFilter: async function () {
axios.get(`http://127.0.0.1:8000/product?filter=${this.filter}`)
.then((response) => {
this.error = null;
this.products = response.data.products;
})
.catch((error) => {
this.error = 'Ошибка! Не могу связаться с API. ' + error
})
}
},
}
</script>
Итак, смотрим как это работает:

Ну вот и всё 🙂 Давай подведём итоги того, чему мы сегодня научились:
- Развернули проект с минимальной базы, подтягивая руками все пакеты
- Научились работать с БД в IDE
- Научились писать фикстуры и использовать рандомайзер имён
- Затронули библиотеку lodash и axios, научились лимитировать обращения к методам, отправлять запросы и получать ответ от бэка
- Немного поигрались с CORS-политикой
Спасибо всем за обратную связь и за то, что это читаете 🙂