Первым делом, расскажу о том, что такое CRUD и зачем нам это вообще нужно.
CRUD — это аббревиатура по первым буквам этого слова. Каждая из которых имеет своё обозначение:
- С — create
- R — read
- U — update
- D — delete
По сути, это просто набор базовых операций над некоторой таблицей в базе данных.
В symfony, мы пользуемся ORM(Object-Relational Mapping), которая скрывает от нас работу с таблицей напрямую, и позволяет работать с нашей базой по правилам ООП. Поэтому, в symfony приложениях мы, как правило, не работаем напрямую с нашей базой, а работаем с объектами.
Теперь о сущностях. Сущность — это модель, по которой строится наша табличка в базе. Из себя она, как правило, представляет обычный ооп класс, с той лишь разницей, что над каждым из полей имеются аннотации, которые описывают это поле в базе. Вот собственно, и вся теория на сегодня. Теперь к практике.
Для начала нам нужно развернуть наше приложение, и обернуть его в докер.
Для этого:
mkdir crud
cd crud/
composer create-project symfony/skeleton crud
Заходим в наше приложение, и создаём папку, называем её docker, в неё кладём содержимое этого репозитория: https://github.com/ko4a/dockerFiles
После этого вытащите файл docker-compose.yml из папки docker, и положите её в общую директорию проекта.
Должна получиться следующая структура файлов:

После этого можно собрать и поднять докер
docker-compose build
docker-compose up -d
Обе команды займут некоторое время на выполнение, так что особо не удивляйтесь этому, это нормально.
После того, как они выполнятся, проверим, что всё развернулось нормально:
docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------------
chat docker-php-entrypoint php-fpm Up 0.0.0.0:8000->10000/tcp, 9000/tcp
crud_nginx_1 nginx Up 0.0.0.0:8080->80/tcp
lesson-mysql docker-entrypoint.sh mysqld Up 0.0.0.0:33061->3306/tcp, 33060/tcp
php docker-php-entrypoint php-fpm Up 0.0.0.0:9001->9000/tcp
После этого, добавим алиас, который будет запускать bin/console из под контейнера с php.
alias sf="docker-compose exec --user www-data php php -d memory_limit=4096M bin/console"
Дальше нам понадобится MakerBundle (можно работать и без него, но так никто не делает), и пакет для работы с нашей ORM (doctrine)
Установить их нжуно из под докер контейнера, в котором у нас крутится php. Для этого:
docker-compose exec php composer require symfony/maker-bundle
docker-compose exec php composer require symfony/orm-pack
После этого, нужно настроить подключение к базе данных. Тут нужно кое-что прояснить.
Первое — для того, чтобы подключиться к базе данных, нужно в .env
файле прописать строку подключения к базе.
Второе — в гите, как правило не хранят никакие пароли или другие конфеденциальные данные. Для этого в основной директории проекта создаётся файл, который называется .env.local
Работает это так: файл .env не добавляется в gitignore, а .env.local добавляется. Каждый разработчик локально у себя хранит его, в котором хранятся все пароли, и другая секретная информация.Если интересно, .gitignore в этом случае выглядит плюс-минус так:
/.env.local
/.env.local.php
/.env.*.local
Для symfony первым приоритетом является .env.local, т.е. сначала она идёт и смотрит окружение, описанное там, если не находит, идёт в .env.
Создадим рядом с .env файл .env.local, и скопируем содержимое из .env, заменив
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
на
DATABASE_URL=mysql://root:mysql@mysql/localdatabase
По строке в принципе понятно что и куда надо поставить. Вам не нужно подставлять свои данные, подставляйте те же, что и в контейнере mysql (если взяли мой контейнер, то можете повторить один в один за мной).
Теперь, наконец, можем приступить к разработке самого CRUD. Первым делом, нам нужна сущность. Пусть это будет product.
sf make:entity
Дальше симфони предоставит вам консольный интерфейс для создания сущности.
Я создал два поля name,price (имя и цена). Выглядело это так
Class name of the entity to create or update (e.g. DeliciousElephant):
> Product
created: src/Entity/Product.php
created: src/Repository/ProductRepository.php
Entity generated! Now let's add some fields!
You can always add more fields later manually or by re-running this command.
New property name (press <return> to stop adding fields):
> name
Field type (enter ? to see all types) [string]:
> string
Field length [255]:
> 64
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Product.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> price
Field type (enter ? to see all types) [string]:
> integer
Can this field be null in the database (nullable) (yes/no) [no]:
> no
updated: src/Entity/Product.php
Add another property? Enter the property name (or press <return> to stop adding fields):
>
Success!
Next: When you're ready, create a migration with make:migration
Симфони предлагает нам делать миграцию. Чтобы не перегружать статью — вместо миграции воспользуемся командой doctrine:schema:update. Эта команда сравнивает ваш текущий код (кстати, можете посмотреть код, который сгенерировала ваша команда — папка Entity, файл Product.php, папка Repository, файл ProductRepository), с текущим состоянием базы, и выполняет некоторые запросы к базе для того, чтобы ваша база совпадала с вашим кодом.
sf doctrine:schema:update --force
Updating database schema...
1 query was executed
[OK] Database schema updated successfully!
Теперь создадим фикстуру. Это такой код, который будет наполнять нашу базу некоторыми тестовыми данными, с которыми мы будем работать.
docker-compose exec php composer require orm-fixtures
sf make:fixture
The class name of the fixtures to create (e.g. AppFixtures):
> ProductFixture
created: src/DataFixtures/ProductFixture.php
Success!
Теперь открываем файлик, и смотрим что тут у нас.Единственный метод load. Я описал его так.
CONST MAX_PRODUCTS = 100;
public function load(ObjectManager $manager)
{
for ($i = 0; $i < self::MAX_PRODUCTS; $i++) {
$product = new Product();
$product->setName('product №'.$i);
$product->setPrice(rand(100,1000));
$manager->persist($product);
}
$manager->flush();
}
Дальше выполняем команду
sf d:f:l
Вылезет предупреждение, о том, что база будет очищена. Соглашаемся.
Поздравляю! Вы наполнили базу фикстурой. Посмотрим, что там.
docker-compose exec mysql bash
mysql -uroot -pmysql
use localdatabase;
SELECT * FROM product;
После этого выходим с mysql и контейнера, дважды выполнив
exit
Самое время создать контроллер.
sf make:controller
Choose a name for your controller class (e.g. VictoriousChefController):
> ProductController
created: src/Controller/ProductController.php
Success!
Открываем, и переопределяем метод index(), прокинув через конструктор ProductRepository(если интересно откуда он взялся, читай про DI в symfony).
/**
* @var ProductRepository
*/
private $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
/**
* @Route("/product", name="product")
*/
public function index()
{
return $this->render('index.html.twig',['products' => $this->productRepository->findAll()]);
}
Шаблоны. Для начала:
docker-compose exec php composer require symfony/twig-bundle
Теперь в папке проекта создадим директорию templates, в ней base.html.twig, index.html.twig.
base будет выглядеть так:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
{% endblock %}
</head>
<body>
<div class="container">
{% block body %}{% endblock %}
</div>
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
Это наша базовая страничка. на которой мы подключаем Bootstrap, и необходимые для него скрипты.
Дальше опишем наш index
{% extends 'base.html.twig' %}
{% block title %}Product index{% endblock %}
{% block body %}
<h1>PRODUCTS!!!</h1>
<a href="#">Create new</a>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Price</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>{{ product.price }}</td>
<td>
<a href="#" class="btn btn-link">show</a>
<a href="#" class="btn btn-danger">edit</a>
<a href="#" class="btn btn-dark">delete</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="3">no records found</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
Теперь можете зайти на localhost:8080/product, и посмотреть что вы сделали.
Теперь перейдем к разработке show метода. В текущем шаблоне замените:
<a href="#" class="btn btn-link">show</a>
На
<a href="{{ path('show_product',{'id':product.id}) }}" class="btn btn-link">show</a>
Тут мы передеали id конкретного продукта в controller, на роут с именем show_product. Такого роута у нас к сожалению ещё нет, пора бы его создать. Идём в контроллер.
/**
* @param Product $product
* @Route("/product/{id}",name="show_product",requirements={"id"="\d+"})
*/
public function show(int $id)
{
$product =$this->productRepository->findOneBy(['id'=>$id]);
return $this->render('show.html.twig',['product' => $product]);
}
Здесь мы просто дёргаем с репозитория объкт с пришедшим id, и выдаём его на страничку(есть ещё путь с автовайрингом, но его посмотрите сами в конце статьи).Параметр requirements жёстко задаёт, что id должен быть типа int. Это нужно для того, чтобы мы в будущем смогли строить url-ы, типа «/product/new».
Сама страничка выглядит так:
{% extends 'base.html.twig' %}
{% block title %}Product{% endblock %}
{% block body %}
<h1>Product</h1>
<table class="table">
<tbody>
<tr>
<th>Id</th>
<td>{{ product.id }}</td>
</tr>
<tr>
<th>Name</th>
<td>{{product.name }}</td>
</tr>
<tr>
<th>Price</th>
<td>{{product.price }}</td>
</tr>
</tbody>
</table>
<a href="{{ path('product') }}">back to list</a>
{% endblock %}
Отлично, возвращаемся в index.html.twig и меняем
<a href="#" class="btn btn-danger">edit</a>
На
<a href="{{ path('edit_product',{'id':product.id}) }}" class="btn btn-danger">edit</a>
Снова идём в контроллер. Пробрасываем в конструктор EntityManagerInterface
/**
* @var ProductRepository
*/
private $productRepository;
/**
* @var EntityManagerInterface
*/
private $manager;
public function __construct(ProductRepository $productRepository,EntityManagerInterface $manager)
{
$this->productRepository = $productRepository;
$this->manager = $manager;
}
И описываем два метода. Один на GET запрос другой на POST.
/**
* @Route("/product/edit/{id}",name="edit_product",methods={"GET"})
*/
public function getEditForm(int $id)
{
$product =$this->productRepository->findOneBy(['id'=>$id]);
return $this->render("edit.html.twig",['product'=>$product]);
}
/**
* @Route("/product/edit/{id}",name="edit",methods={"POST"})
*/
public function edit(int $id,Request $request)
{
$product =$this->productRepository->findOneBy(['id'=>$id]);
$product->setPrice($request->request->get('price'));
$product->setName($request->request->get('name'));
$this->manager->persist($product);
$this->manager->flush();
return $this->redirectToRoute('product');
}
Теперь по порядочку. Первый метод — getForm. Просто напросто возвращает страничку, на которой находится наша формочка для редактирования сущности, вместе с этой сущностью.
Сама страничка называется edit.html.twig и выглядит так:
{% extends 'base.html.twig' %}
{% block body%}
<form method="post">
<div class="form-group">
<label for="name">Product Name</label>
<input type="text" class="form-control" name="name" value="{{ product.name }}">
</div>
<div class="form-group">
<label for="product-price">Price</label>
<input type="number" class="form-control" id="product-price" name="price" value="{{ product.price }}">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}
Она шлёт POST запрос по тому уже url-у, на котором и находится. Тут её ловит второй метод
edit. Дальше мы просто вытаскиваем из запроса нужные параметры, и ставим соответствующей сущности в базе эти параметры. И сохраняем изменённый объект в базу, после чего редиректим пользователя на основную страничку.
Теперь удаление. Пожалуй, самое простое.
Как обычно,в index меняем
<a href="#" class="btn btn-dark">delete</a>
На
<a href="{{ path('delete_product',{'id':product.id}) }}" class="btn btn-dark">delete</a>
В контроллере создаём соответствующий метод:
/**
* @Route("/product/delete/{id}",name="delete_product")
*/
public function delete(int $id)
{
$this->manager->remove($this->productRepository->findOneBy(['id'=>$id]));
$this->manager->flush();
return $this->redirectToRoute('product');
}
Здесь мы просто пошли в репозиторий, нашли соответствующий экземпляр, и удалили его. После чего редиректнули пользователя на главную.
Осталось создание. Делается аналогично изменению:
В индексе меняем
<a href="#">Create new</a>
На
<a href="{{ path('create_product') }}">Create new</a>
В контроллере определяем два метода:
/**
* @Route("/product/new",name = "new_product",methods={"GET"})
*/
public function getCreateForm()
{
return $this->render('create.html.twig');
}
/**
* @Route("/product/new",name = "create_product",methods={"POST"})
*/
public function new(Request $request)
{
$product = new Product();
$product->setName($request->request->get('name'));
$product->setPrice($request->request->get('price'));
$this->manager->persist($product);
$this->manager->flush();
return $this->redirectToRoute('product');
}
Всё аналогично изменению. Разметка create.html.twig выглядит так:
{% extends 'base.html.twig' %}
{% block body %}
<form method="post">
<div class="form-group">
<label for="name">Product Name</label>
<input type="text" class="form-control" name="name">
</div>
<div class="form-group">
<label for="product-price">Price</label>
<input type="number" class="form-control" id="product-price" name="price">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}
Ну, вот и всё. Полноценный CRUD написан. Теперь удалите контроллер, и все шаблоны. Есть альтернативный путь, более лёгкий. Сейчас покажу. Удаляем контроллер, и шаблоны, после этого.
docker-compose exec php composer require form validator security-csrf annotations
sf make:crud
Вот и всё. CRUD готов, можете наслаждаться.
Приложение, которое мы писали своими руками можно посмотреть тут: https://github.com/ko4a/CRUD-symfony