Пишем CRUD на Symfony 4

Первым делом, расскажу о том, что такое 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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *