- Установка
- Команды
- Сохранение данных
- Запрос данных
- Entity Repositories
- Добавление новых полей
- Установка значения по умолчанию
- Fixture
- Extentions for Doctrine
-
composer require doctrine- скачиваем файлы с пакетного менеджера -
В файле
.envдобавить новую переменную окружения(или будет уже добавленно):
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
Данная переменная используется в конфиге config/packages/doctrine.yaml. Так же тут есть другие настройки как charset, driver, server_version и т.д.
bin/console doctrine:database:create - создаст новую базу данных, с именем которое указанно в db_name из пункта №2 выше.
bin/console make:entity - команда сервиса MakerBundle, которая позволяет в полуавтоматическом режиме создавать сущности(Entity) для связи приложение <-> база данных. В итоге будет создан простой php class в котором имя = названию таблицы в базе данных, а поля(свойства) класса - столбцы таблицы. Doctrine для своей работы испльзуется аннотации.
bin/console make:migration - команда, которая создаёт файл миграции на основании Entity. Doctrine читает этот файл и в атоматическом режиме создаёт sql-команды. Каждая новая уникальная миграция создаётся со своим порядковым номером
bin/console doctrine:migrations:migrate - выполнить миграции. После использования этой комманды Doctrine возьмет файлы из src/Migrations/ и выполнит sql-команды которые находятся внутри. Для предотвращения повторной(дублирующей) миграции Docrtine создаёт новую таблицу с именем migration_varsions куда заносит данные о произведенных миграциях.
bin/console make:fixture - создание новой Fixture. Fixture - это такой бандл(инструмент) который позволяет загружать фейковые данные в базу данных. Используется в среде dev для тестирования. Подробнее об этом инструменте написанно ниже
bin/console doctrine:fixture:load - загрузить данные в бд с помощью Fixture. ВНИМАНИЕ перед загрузкой таблица будет очищена!
Допустим с помощью команды bin/console make:entity и bin/console make:migration была создана сущность(таблица в БД) Article с полями title, slug, content. Теперь мы можешь создать отдельный класс который будет позволять соранять данные в тиблицу Article - например ArticleAdminController.
Внутри данного класса мы можем оперировать методами класса Article (src/Entity/Article.php), которые позволяют проводить операции с контентом(напр. setTitle), таким образом мы подготоваливаем наши данные(контент) к сохранению.
Для сохранения данных необходимо воспользоваться EntityManagerInterface (см. список всех сервисов bin/console debug:autowiring), установив его через DI. Необходимо всего 2 метода:
$em->persist($article);- подготовить данные. Необходимо для возможности подготовить сразу 10 статей - оптимизация процесса.$em->flush();- непосредственно сохранение данных(командаINSERT)
Запрос данных делается так же с использование EntityManagerInterface. Для этого нам необходимо передать имя класса в метод
getRepository().
$repository = $em->getRepository(Article::class);
Вместо того, что б использовать
EntityManagerInterfaceи получать объект репозитория черезgetRepository(), можно сразу использоватьEntityRepositoryпри подключении черезDI(что это за файл см. ниже)
Теперь для $repository нам будут доступны несколько методов:
find()- требует$idчто б получить одну конкретную статьюfindAll()- получить все статьиfindBy()- получить все статьи, где поле строки соответствует некоторому переданному значениюfindOneBy()- получить одну статью, где поле строки соответствует некоторому переданному значению
Для примера со статьями используем findOneBy() со поиском по $slug:
$article = $repository->findOneBy(['slug' => $slug]);
Статьи с таким
$slugможет и не быть. В таком случае$articleбудет равноnullи нам необходимо выполнить обработку подобного поведения. Добавим исключение:
if (!$article) {
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug));
}
где $this->createNotFoundException является трейтом, который расширяет класс AbstractController(это основной класс от которого наследуются все контроллеры).
Если распечатать $article то можно увидеть что это объект со свойствами, имя которых соответствует каждому столбцу в таблице.
При использовании команды bin/console make:entity для автоматической генерации entity будет создан файл EntityRepository.php. В котором есть возможность для описания собственного запроса данных, помимо предоставленных по умолчанию.
Doctrine позволяет писать пользовательские запросы с помощью Query Builder. Пример нового запроса, который возвращает посты в которые дата публикации присутсвует и с сортировкой:
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.publishedAt IS NOT NULL')
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult()
;
}
где:
createQueryBuilder('a')-aэто псевдомин таблицыandWhere()иorderBy()- методы которые соответствуютAND WHEREиORDER BYgetQuery()- подготавливает запросgetResult()- выполняет запрос. Так еще может существоватьgetOneOrNullResultкоторый вернет один результат или ничего.
Например есть метод
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.publishedAt IS NOT NULL')
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult()
;
}
- Изолируем логику запроса. Для этого сделаем приватный метод
addIsPublishedQueryBuilder()и аргументомDoctrine\ORM $qb:
private function addIsPublishedQueryBuilder(QueryBuilder $qb = null)
{
return $this->getOrCreateQueryBuilder($qb)
->andWhere('a.publishedAt IS NOT NULL');
}
private function getOrCreateQueryBuilder(QueryBuilder $qb = null)
{
return $qb ?: $this->createQueryBuilder('a');
}
Метод findAllPublishedOrderedByNewest() после всех изменений:
public function findAllPublishedOrderedByNewest()
{
return $this->addIsPublishedQueryBuilder()
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult()
;
}
Для добавления нового поля в уже существующую entity можно воспользоваться 2-я вариантами
- добавить вручную в файл
src/Entity/Article.phpи сделать миграции - автоматически сгенерировать новые поля через команду
bin/console make:entity, указав имя класса которое необходимо обновить и указать какие новые поля требуется добавить.
Второй способ естественно предпочтительнее.
После выполнения команды из пункта 2, файл src/Entity/Article.php будет автоматически обновлён и в нём добавяться свойства и методы для работы с новыми полями. Далее требуется выполнить миграции с помощью команд bin/console make:migration и bin/console make:migration
В файле src/Entity/Article.php для требуемого свойства установить присвоить какое-либо значение.
Например, мы добавили новое поле $heartCount которое отвечает за количество лайков у конкретного поста, и хотим что б данное значение равнялось 0 при первичном создании поста. В коде это будет так:
/**
* @ORM\Column(type="integer")
*/
private $heartCount = 0;
Это классы php которые предназначены для создания entity и загрузки тестовых данных в базу данных.
composer require --dev orm-fixtures - установка DoctrineFixturesBundle
bin/console make:fixtures - создание fixture. После ввода имени появится файл src/DataFixtures/NameFixtures.php
Использование: в метод load() помещаем создание объекта требуемого entity и заполняем там же данными. Для загрузки используем команду в консоле bin/console doctrine:fixtures:load. Для примера ниже будет загружена всего одна запись.
ОБРАТИТЬ ВНИМАНИЕ использование данной команды удалит все записи в таблице и запишет заново
Пример:
namespace App\DataFixtures;
use App\Entity\Article;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class ArticleFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
$article = new Article();
$article->setTitle('Why Asteroids Taste Like Bacon')
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999))
->setContent(<<<EOF
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow,
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit
labore minim pork belly spare ribs cupim short loin in.
EOF
);
);
// publish most articles
if (rand(1, 10) > 2) {
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100))));
}
$article->setAuthor('Mike Ferengi')
->setHeartCount(rand(5, 100))
->setImageFilename('asteroid.jpeg')
;
$manager->persist($article);
$manager->flush();
}
}
Для создания сразу нескольких статей, можно пойти двумя путями:
В методе load() просто использовать цикл
public function load(ObjectManager $manager)
{
for ($i = 0; $i < 10; $i++) {
$article = new Article();
$article->setTitle('Why Asteroids Taste Like Bacon')
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999))
...
;
$manager->persist($article);
}
$manager->flush();
}
- Создать абстрактный класс
BaseFixture, который будет наследоваться отFixture. - Тут же реализуем метод
load(), который принимает в качестве аргументаObjectManager $managerи устанавливает его как свойство данного класса(BaseFixture) - Создать абстрактный метод
loadData()с те же аргументомObjectManager $manager - в
load()делаем вызовloadData()
Должно получиться:
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
abstract class BaseFixture extends Fixture
{
/** @var ObjectManager */
private $manager;
abstract protected function loadData(ObjectManager $manager);
public function load(ObjectManager $manager)
{
$this->manager = $manager;
$this->loadData($manager);
}
}
- Далее создадим метод который будет отвечать за какую fixtur'e необходимо формировать
protected function createMany(string $className, int $count, callable $factory)
{
for ($i = 0; $i < $count; $i++) {
$entity = new $className();
$factory($entity, $i);
$this->manager->persist($entity);
$this->addReference($className . '_' . $i, $entity);
}
}
где:
$className- передаём какого класса хотим создать fixture'e$count- количество записейcallable $factory- функция обратного вызова, которая будет вызываться каждый раз когда создаётся объект.$this->addReference($className . '_' . $i, $entity);- используется для связи между несколькимиfixture. Подброднее ниже
- Теперь в файле
src/DataFixtures/ArticleFixtures.php(а мы все примеры делаем тут дляArticle) убираем наследование отFixtureи наследуемся отBaseFixture. Реализуем методloadData()в котором происходит вызовcreateManyс передачей необходимых нам аргументов$className,$countи анонимной функции(в которой описываем все манипуляции с данными)
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) {
$article->setTitle('Why Asteroids Taste Like Bacon')
->setSlug('why-asteroids-taste-like-bacon-'.$count)
...
});
$manager->flush();
}
Еще раз резюмируя:
-
С помощью команды
bin/console doctrine:fixture:loadзапускается методload()в классе который наследуется отFixture, у нас это абстрактный классBaseFixture, от которого в свою очередь наследуетсяArticleFixture. -
В
BaseFixtureсодержиться логика
- для загрузки необходимых нам данных - это собственно метод
loadData()и его вызов$this->loadData($manager);(который находиться внутриload()). - метод
createMany()который принимает в себя Класс, количество итераций и анонимную функцию, в которой описанно в какие поля таблицы нужно заносить какие данные. Т.е. по факту тут происходит абстракция логики - этот метод нужен для подготовки данных$this->manager->persist($entity);в каждой итерации.
- В наследуемом класс
ArticleFixtureописываем логикуloadData()дляcreateMany()и завершая запись в БД$manager->flush();
При использовании нескольких файлов fixture появляется потребность использовать объекты в разных файлах. Данный метод позволяет создать ссылку на такие объекты, а затем с помощью getRefference() получить этот объект через его имя.
Т.е. у нас есть fixture которая отвечает за генерацию Article и для этой сущности использум $this->addReference($className . '_' . $i, $entity);, где $i используется для обозначения каждого объекта. Теперь в другой fixture можно использовать $this->getReference(Article::class.'_'.$i) для того, что б получить конкретный(указав при этом конкретный $i) объект Article.
Doctrine загружает файлы fixture в алфовитном порядке. Однако возникает ситуация, когда фикстура B требует данных с фикстуры D. В таком случае как только класс фикстуры зависит от другого класса, необходимо реализовать интерфейс с именем DependentFixtureInterface.
Вернёмся к нашему примеру. Сейчас у нас есть только фикстура ArticleFixture. Допустим появляется фикстура A0SomethingFixture которая у себя использует объекты ArticleFixture. Для разрешения конфикта реализуем DependentFixtureInterface с методом getDependencies(), где просто вернем массивом метод от которого есть зависимость.
class A0SomethingFixture extends Fixture implements DependentFixtureInterface
{
public function getDependencies()
{
return [ArticleFixtures::class];
}
}
В качестве примера у нас будут комментарии которые можно оставить к каждой статье. Данные отношения описываются как Много-к-одному(Many-To-One m-2-o) со стороны комментариев, т.е. много комментариев могут быть на одной записи. И Один-ко-многим(One-To-Many o-2-m) со стороны записи, т.е. каждая запись может иметь много комментариев.
Мы уже создали entity для комментариев, теперь необходимо связать его с Article, путем добавления нового столбца, с именем наподобие articleId. Для этого используем команду bin/console make:entity, но при запросе New property name пишем article, а при Field type - relation. relation - даёт понять, что данное поле будет обозначать связь между двумя таблицами и запустит специальный интерфейс для описания этой связи.
Первый вопрос - с каким классом должна быть связана эта entity? В нашем примере это Article. Далее необходимо выбрать каким будет эта связь - ManyToOne, OneToMany, ManyToMany, OneToOne. На этот вопрос был дан ответ в первом абзаце - ManyToOne(кроме того интерфейс даёт подсказки как это выглядит).
Далее необходимо ответить на вопрос будет ли добавляемое поле таблицы nullable - no, не может быть т.к. у каждый комментарий так или иначе будет принадлежать к какой-либо статье.
Следующий вопрос касается возможности добавления в Article дополнительных методов по работе с Comments, например нужно ли добавлять метод $article->getComments() - yes, да нужно, т.к. мы хотим иметь возможность получить все комментарии для определённой записи. После необходимо ввести имя нового свойства для Article которое будет соответствовать Comments - comments.
!!!
В конце будет вопрос является ли это связь orphanRemoval, что означает, если в Article не будет ссылок на Comments, то последние должны быть удалены при удалении Article - ПЕРЕФРАЗИРОВАТЬ
!!!
В итоге:
- в файле
src/Entity/Comment.phpпоявится новое свойствоarticleс необходимо аннотацией:
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $article;
- в файле
src/Entity/Article.php-commentsи методы для работы сentity Comments
/**
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article")
*/
private $comments;
/**
* @return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setArticle($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
// set the owning side to null (unless already changed)
if ($comment->getArticle() === $this) {
$comment->setArticle(null);
}
}
return $this;
}
есть особенность: так же необходимо создать конструктор который объявляет свойство
commentsкакnew ArrayCollection()
public function __construct()
{
$this->comments = new ArrayCollection();
}
В конце делаем миграции и связь между двумя таблицами установлена!
Т.к. при генерации отношения в связываемом объекте, мы добавили метод $article->getComments(), Symfony будет использовать LazyLoad для сущности Comment при вызове сущности Article.
Т.е. когда идет обращение к сущности Article Doctrine и Symfony знают о том, что Article и Comment связаны между собой, и запрос к базе данных не будет выполен до тех пор, пока комменатрии явно не будут вызваны. Если есть вызов, то запрос в БД произойдет в фоновом режиме.
При установке связи между двумя сущностями, есть возможность для сохранения данных как с помощью первой так и второй сущности. Однако важно понимать, что только одна сущность будет владельцем, а другая - как бы обратная сторона отношения.
Для нашего примера владельцем будет сущность Comment. За этот факт еще говорит то, что сторона-владелец - это так сторона, где фактически столбец появляется в базе данных. Для нас это article_id в таблице Comment.
Т.е. если мы захотим сохранить комментарий через сущность article(ниже код из темы про fixture):
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) {
$article->setTitle('Why Asteroids Taste Like Bacon')
...
$comment1 = new Comment();
$comment1->setAuthorName('Mike Ferengi');
$comment1->setContent('I ate a normal rock once. It did NOT taste like bacon!');
$comment1 = new Comment();
$comment1->setAuthorName('Mike Ferengi');
$comment1->setContent('I ate a normal rock once. It did NOT taste like bacon!');
$article->addComment($comment1);
$article->addComment($comment2);
});
$manager->flush();
}
то внутри entity Article будет вызван метод addComment:
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setArticle($this);
}
return $this;
}
который сперва проверит не будет ли дублироваться новый комментарий, а во-вторых передаст управление сохранением комментария в сущность Comment.
Допустим у нас есть форма, которая просто посылает GET-запрос. На стороне контроллера мы можем поймать этот запрос с помощью Symfony\Component\HttpFoundation\Request. конечно использовав его как подсказка аргумента в методе:
public function index(CommentRepository $repository, Request $request)
{
...
}
Не забудь! В подсказку аргумента метода передаются:
type-hint services,type-hint entitiesиtype-hint the Request class.
Теперь можно получить значение переданного запроса
public function index(CommentRepository $repository, Request $request)
{
$q = $request->query->get('q');
...
}
где $request->query - отвечает за $_GET. Так же можно использвовать $request->headers для заголовков, $request->cookies, $request->files и так далее.
Теперь необходимо построить запрос в базу данных по полученному значению. Что б данный запрос был универсальным, добавим возможность выборки и при нулевом значении $requst.
Создаём новый запрос в src/Repository/CommentRepository.php (т.к. мы вообще тут с данной Entity и работает)
public function findAllWithSearch(?string $term)
{
}
Внутри:
- Начнем создавать запрос -
$qb = $this->createQueryBuilder('c'); - Если
$termпередан сформируем строку запроса для поиска(используемLIKE) фразы по контенту или по автору:
if ($term) {
$qb->andWhere('c.content LIKE :term OR c.authorName LIKE :term')
->setParameter('term', '%' . $term . '%')
;
}
По-рекомендации авторов - всегда использовать конструкцию
andWhere(), а неorWhere(), т.к. существует потенциальная вероятность в ошибке использования скобок. В результате получившийся результат выборки может значительно превышать ожидаемый.
- Делаем сортировку и получаем результат. Полный метод:
public function findAllWithSearch(?string $term)
{
$qb = $this->createQueryBuilder('c');
if ($term) {
$qb->andWhere('c.content LIKE :term OR c.authorName LIKE :term')
->setParameter('term', '%' . $term . '%')
;
}
return $qb
->orderBy('c.createdAt', 'DESC')
->getQuery()
->getResult()
;
}
Запрос поиска будет произведен только по тексту и авторам комментариев, т.е. только в сущности
Comment
В итоге первоначальный метод index() примет такой вид:
public function index(CommentRepository $repository, Request $request)
{
$q = $request->query->get('q');
$comments = $repository->findAllWithSearch($q);
return $this->render('comment_admin/index.html.twig', [
'comments' => $comments,
]);
}
Добавим возможность поиска и по заголовкам статей. В этом случае мы хотим присоеденится от comment к article. Сделать это можно с помощью ->innerJoin('c.article', 'a'). Теперь запрос так же выбирает и статьи. Используем это внутри andWhere():
public function findAllWithSearch(?string $term)
{
$qb = $this->createQueryBuilder('c')
->innerJoin('c.article', 'a');
if ($term) {
$qb->andWhere('c.content LIKE :term OR c.authorName LIKE :term OR a.title LIKE :term')
->setParameter('term', '%' . $term . '%')
;
}
return $qb
->orderBy('c.createdAt', 'DESC')
->getQuery()
->getResult()
;
}
Используй ->addSelect('a');.
!!! ОПИСАТЬ БОЛЬШЕ !!!