<? phpukraine ДОКУМЕНТАЦІЯ
Пошук по платформі
Документація українською

Переклад офіційної документації українською. Кожен розділ показує стан готовності: недоперекладене позначене відкрито, а не приховане.

ВЕРСІЯ
АКТУАЛЬНА Актуальний реліз. Переклад наздоганяє оригінал — розділи з низьким відсотком позначені у змісті.
DOCTRINE · ПЕРЕКЛАДЕНО оновлено 5 вересня 2026

Звʼязки сутностей — Symfony

Скрінкаст

Віддаєте перевагу відеоурокам? Перегляньте серію скрінкастів Mastering Doctrine Relations.

Є два основні типи звʼязків/асоціацій:

ManyToOne / OneToMany : Найпоширеніший звʼязок, який у базі даних відображається колонкою зовнішнього ключа (наприклад, колонкою category_id у таблиці product). Насправді це лише один тип асоціації, але побачений з двох різних боків звʼязку.

ManyToMany : Використовує проміжну таблицю і потрібен тоді, коли кожен бік звʼязку може мати багато обʼєктів іншого боку (наприклад, «студенти» і «класи»: кожен студент відвідує багато класів, і в кожному класі багато студентів).

Спершу потрібно визначити, який звʼязок використовувати. Якщо обидва боки звʼязку міститимуть багато обʼєктів іншого боку (наприклад, «студенти» і «класи»), потрібен звʼязок ManyToMany. Інакше вам, найімовірніше, потрібен ManyToOne.

Порада

Існує також звʼязок OneToOne (наприклад, один User має один Profile і навпаки). На практиці його використання схоже на ManyToOne.

Асоціація ManyToOne / OneToMany

Припустімо, що кожен продукт у вашому застосунку належить рівно до однієї категорії. У цьому випадку вам знадобиться клас Category і спосіб повʼязати обʼєкт Product з обʼєктом Category.

Почніть зі створення сутності Category з полем name:

$ php bin/console make:entity Category

New property name (press <return> to stop adding fields):
> name

Field type (enter ? to see all types) [string]:
> string

Field length [255]:
> 255

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

Це згенерує новий клас сутності:

// src/Entity/Category.php
namespace App\Entity;

// ...

#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private $id;

    #[ORM\Column]
    private string $name;

    // ... геттери та сеттери
}

Порада

Ви можете передати команді make:entity параметр --with-uuid або --with-ulid. Використовуючи компонент Symfony Uid, це згенерує сутність, у якої тип id буде Uuid або Ulid замість int.

Мапінг звʼязку ManyToOne

У цьому прикладі кожна категорія може бути повʼязана з багатьма продуктами. Але кожен продукт може бути повʼязаний лише з однією категорією. Цей звʼязок можна підсумувати так: багато продуктів до однієї категорії (або, що те саме, одна категорія до багатьох продуктів).

З погляду сутності Product це звʼязок many-to-one. З погляду сутності Category це звʼязок one-to-many.

Щоб це змапити, спершу створіть властивість category у класі Product з атрибутом ManyToOne. Це можна зробити вручну або за допомогою команди make:entity, яка поставить вам кілька запитань про ваш звʼязок. Якщо ви не впевнені у відповіді — не хвилюйтеся! Ви завжди зможете змінити налаштування пізніше:

$ php bin/console make:entity

Class name of the entity to create or update (e.g. BraveChef):
> Product

New property name (press <return> to stop adding fields):
> category

Field type (enter ? to see all types) [string]:
> relation

What class should this entity be related to?:
> Category

Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToOne

Is the Product.category property allowed to be null (nullable)? (yes/no) [yes]:
> no

Do you want to add a new property to Category so that you can access/update
Product objects from it - e.g. $category->getProducts()? (yes/no) [yes]:
> yes

New field name inside Category [products]:
> products

Do you want to automatically delete orphaned App\Entity\Product objects
(orphanRemoval)? (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

Це внесло зміни у дві сутності. По-перше, до сутності Product додано нову властивість category (а також методи-геттер і сеттер):

PHP-атрибути

// src/Entity/Product.php
namespace App\Entity;

// ...
class Product
{
    // ...

    #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')]
    private Category $category;

    public function getCategory(): ?Category
    {
        return $this->category;
    }

    public function setCategory(?Category $category): self
    {
        $this->category = $category;

        return $this;
    }
}

YAML

# src/Resources/config/doctrine/Product.orm.yml
App\Entity\Product:
    type: entity
    # ...
    manyToOne:
        category:
            targetEntity: App\Entity\Category
            inversedBy: products
            joinColumn:
                nullable: false

XML

<!-- src/Resources/config/doctrine/Product.orm.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
        https://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

    <entity name="App\Entity\Product">
        <!-- ... -->
        <many-to-one
            field="category"
            target-entity="App\Entity\Category"
            inversed-by="products">
            <join-column nullable="false"/>
        </many-to-one>
    </entity>
</doctrine-mapping>

Цей мапінг ManyToOne є обовʼязковим. Він каже Doctrine використовувати колонку category_id у таблиці product, щоб повʼязати кожен запис цієї таблиці із записом у таблиці category.

Далі, оскільки один обʼєкт Category буде повʼязаний з багатьма обʼєктами Product, команда make:entity також додала до класу Category властивість products, яка міститиме ці обʼєкти:

PHP-атрибути

// src/Entity/Category.php
namespace App\Entity;

// ...
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

class Category
{
    // ...

    #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category')]
    private Collection $products;

    public function __construct()
    {
        $this->products = new ArrayCollection();
    }

    /**
     * @return Collection<int, Product>
     */
    public function getProducts(): Collection
    {
        return $this->products;
    }

    // addProduct() і removeProduct() також були додані
}

YAML

# src/Resources/config/doctrine/Category.orm.yml
App\Entity\Category:
    type: entity
    # ...
    oneToMany:
        products:
            targetEntity: App\Entity\Product
            mappedBy: category
# Не забудьте ініціалізувати колекцію в
# методі __construct() сутності

XML

<!-- src/Resources/config/doctrine/Category.orm.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
        https://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

    <entity name="App\Entity\Category">
        <!-- ... -->
        <one-to-many
            field="products"
            target-entity="App\Entity\Product"
            mapped-by="category"/>

        <!--
            не забудьте ініціалізувати колекцію в
            методі __construct() сутності
        -->
    </entity>
</doctrine-mapping>

Показаний раніше мапінг ManyToOne є обовʼязковим. Але цей OneToMany — необовʼязковий: додавайте його, лише якщо хочете мати доступ до продуктів, повʼязаних із категорією (це одне із запитань, які ставить make:entity). У цьому прикладі можливість викликати $category->getProducts() буде корисною. Якщо вам це не потрібно, то не потрібні й налаштування inversedBy чи mappedBy.

Що це за штука ArrayCollection?

Код усередині __construct() важливий: властивість $products має бути обʼєктом-колекцією, що реалізує інтерфейс Collection від Doctrine. У цьому випадку використовується обʼєкт ArrayCollection. Він виглядає і поводиться майже точно як масив, але має додаткову гнучкість. Просто уявіть, що це array, — і все буде гаразд.

Вашу базу даних налаштовано! Тепер запустіть міграції як зазвичай:

$ php bin/console doctrine:migrations:diff
$ php bin/console doctrine:migrations:migrate

Завдяки звʼязку це створює колонку зовнішнього ключа category_id у таблиці product. Doctrine готова зберігати ваш звʼязок!

Збереження повʼязаних сутностей

Тепер ви можете побачити цей новий код у дії! Уявіть, що ви всередині контролера:

// src/Controller/ProductController.php
namespace App\Controller;

// ...
use App\Entity\Category;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ProductController extends AbstractController
{
    #[Route('/product', name: 'product')]
    public function index(EntityManagerInterface $entityManager): Response
    {
        $category = new Category();
        $category->setName('Computer Peripherals');

        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(19.99);
        $product->setDescription('Ergonomic and stylish!');

        // повʼязує цей продукт із категорією
        $product->setCategory($category);

        $entityManager->persist($category);
        $entityManager->persist($product);
        $entityManager->flush();

        return new Response(
            'Saved new product with id: '.$product->getId()
            .' and new category with id: '.$category->getId()
        );
    }
}

Коли ви перейдете на /product, до таблиць category і product буде додано по одному рядку. Колонка product.category_id для нового продукту отримає значення id нової категорії. Doctrine керує збереженням цього звʼязку за вас:

Doctrine мапить повʼязані сутності Product і Category на таблиці product і category в базі даних

Якщо ви новачок в ORM, це найважча концепція: вам треба перестати думати про базу даних і натомість думати лише про свої обʼєкти. Замість того щоб записувати цілочисельний id категорії в Product, ви записуєте цілий обʼєкт Category. Про решту Doctrine подбає під час збереження.

Оновлення звʼязку з оберненого боку

Чи можна також викликати $category->addProduct(), щоб змінити звʼязок? Так, але лише тому, що вам допомогла команда make:entity. Детальніше див.: Встановлення інформації з оберненого боку.

Отримання повʼязаних обʼєктів

Коли вам потрібно отримати повʼязані обʼєкти, ваш робочий процес виглядає так само, як і раніше. Спершу отримайте обʼєкт $product, а потім зверніться до повʼязаного з ним обʼєкта Category:

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
// ...

class ProductController extends AbstractController
{
    public function show(ProductRepository $productRepository, int $id): Response
    {
        $product = $productRepository->find($id);
        // ...

        $categoryName = $product->getCategory()->getName();

        // ...
    }
}

У цьому прикладі ви спершу робите запит на обʼєкт Product за його id. Це виконує запит, який отримує лише дані продукту й гідрує $product. Пізніше, коли ви викликаєте $product->getCategory()->getName(), Doctrine непомітно виконує другий запит, щоб знайти Category, повʼязану з цим Product. Вона готує обʼєкт $category і повертає його вам.

Doctrine запитує дані Category лише тоді, коли вони потрібні

Важливо те, що ви маєте доступ до категорії, повʼязаної з продуктом, але дані категорії насправді не отримуються, доки ви не запитаєте категорію (тобто вони завантажуються «ліниво»).

Оскільки ми змапили необовʼязковий бік OneToMany, ви можете робити запити й у зворотному напрямку:

// src/Controller/ProductController.php

// ...
class ProductController extends AbstractController
{
    public function showProducts(CategoryRepository $categoryRepository, int $id): Response
    {
        $category = $categoryRepository->find($id);

        $products = $category->getProducts();

        // ...
    }
}

У цьому випадку відбувається те саме: спершу ви робите запит на один обʼєкт Category. Потім, тільки коли (і якщо) ви звертаєтеся до продуктів, Doctrine виконує другий запит, щоб отримати повʼязані обʼєкти Product. Цього додаткового запиту можна уникнути, додавши JOIN.

Звʼязки та проксі-класи

Це «ліниве завантаження» можливе тому, що за потреби Doctrine повертає «проксі»-обʼєкт замість справжнього обʼєкта. Погляньте ще раз на приклад вище:

$product = $productRepository->find($id);

$category = $product->getCategory();

// виводить "Proxies\AppEntityCategoryProxy"
dump($category::class);
die();

Цей проксі-обʼєкт розширює справжній обʼєкт Category, виглядає і поводиться точно як він. Різниця в тому, що завдяки проксі-обʼєкту Doctrine може відкласти запит справжніх даних Category доти, доки ці дані вам справді знадобляться (наприклад, доки ви не викличете $category->getName()).

Проксі-класи генеруються Doctrine і зберігаються в теці кешу. Ви, ймовірно, навіть не помітите, що ваш обʼєкт $category насправді є проксі-обʼєктом.

У наступному розділі, коли ви отримуватимете дані продукту й категорії одразу (через join), Doctrine поверне справжній обʼєкт Category, оскільки нічого не потрібно завантажувати ліниво.

Обʼєднання повʼязаних записів

У прикладах вище виконувалося два запити — один для початкового обʼєкта (наприклад, Category) і один для повʼязаного обʼєкта чи обʼєктів (наприклад, обʼєктів Product).

Порада

Памʼятайте, що всі запити, виконані під час запиту, можна побачити у веб-панелі налагодження (web debug toolbar).

Якщо ви наперед знаєте, що вам знадобиться доступ до обох обʼєктів, ви можете уникнути другого запиту, зробивши join у початковому запиті. Додайте наступний метод до класу ProductRepository:

// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function findOneByIdJoinedToCategory(int $productId): ?Product
    {
        $entityManager = $this->getEntityManager();

        $query = $entityManager->createQuery(
            'SELECT p, c
            FROM App\Entity\Product p
            INNER JOIN p.category c
            WHERE p.id = :id'
        )->setParameter('id', $productId);

        return $query->getOneOrNullResult();
    }
}

Це все одно поверне обʼєкт Product. Але тепер, коли ви викличете $product->getCategory() і скористаєтеся цими даними, другого запиту не буде.

Тепер ви можете використати цей метод у своєму контролері, щоб одним запитом отримати обʼєкт Product і повʼязану з ним Category:

// src/Controller/ProductController.php

// ...
class ProductController extends AbstractController
{
    public function show(ProductRepository $productRepository, int $id): Response
    {
        $product = $productRepository->findOneByIdJoinedToCategory($id);

        $category = $product->getCategory();

        // ...
    }
}

Встановлення інформації з оберненого боку

Досі ви оновлювали звʼязок викликом $product->setCategory($category). Це не випадковість! Кожен звʼязок має два боки: у цьому прикладі Product.category — це володіючий (owning) бік, а Category.productsобернений (inverse) бік.

Щоб оновити звʼязок у базі даних, ви мусите встановити звʼязок на володіючому боці. Володіючий бік — це завжди той, де задано мапінг ManyToOne (для звʼязку ManyToMany ви можете обрати, який бік буде володіючим).

Чи означає це, що неможливо викликати $category->addProduct() або $category->removeProduct(), щоб оновити базу даних? Насправді це можливо — завдяки хитрому коду, який згенерувала команда make:entity:

// src/Entity/Category.php

// ...
class Category
{
    // ...

    public function addProduct(Product $product): self
    {
        if (!$this->products->contains($product)) {
            $this->products[] = $product;
            $product->setCategory($this);
        }

        return $this;
    }
}

Ключовим є $product->setCategory($this), що встановлює володіючий бік. Завдяки цьому під час збереження звʼязок таки оновиться в базі даних.

А як щодо видалення Product з Category? Команда make:entity також згенерувала метод removeProduct():

// src/Entity/Category.php
namespace App\Entity;

// ...
class Category
{
    // ...

    public function removeProduct(Product $product): self
    {
        if ($this->products->contains($product)) {
            $this->products->removeElement($product);
            // встановлюємо володіючий бік у null (якщо його ще не змінили)
            if ($product->getCategory() === $this) {
                $product->setCategory(null);
            }
        }

        return $this;
    }
}

Завдяки цьому, якщо ви викличете $category->removeProduct($product), значення category_id для цього Product буде встановлено в null у базі даних.

Попередження

Зверніть увагу, що обернений бік може бути повʼязаний із великою кількістю записів. Тобто може існувати велика кількість продуктів з однаковою категорією. У такому випадку $this->products->contains($product) може призвести до небажаних запитів до бази даних і дуже високого споживання памʼяті з ризиком складних для налагодження помилок «Out of memory».

Тому переконайтеся, що обернений бік вам справді потрібен, і перевірте, чи не може згенерований код спричинити такі проблеми.

Але що, якщо замість встановлення category_id у null ви хочете, щоб Product було видалено, якщо він стає «сиротою» (тобто без Category)? Щоб обрати таку поведінку, використайте опцію orphanRemoval всередині Category:

PHP-атрибути

// src/Entity/Category.php

// ...

#[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', orphanRemoval: true)]
private array $products;

Завдяки цьому, якщо Product буде видалено з Category, він буде повністю видалений з бази даних.

Докладніше про асоціації

Цей розділ був вступом до одного поширеного типу звʼязків між сутностями — звʼязку one-to-many. Докладніші відомості та приклади використання інших типів звʼязків (наприклад, one-to-one, many-to-many) див. у документації Doctrine з мапінгу асоціацій.

Зверніть увагу

Якщо ви використовуєте атрибути, до всіх атрибутів потрібно додавати префікс #[ORM\] (наприклад, #[ORM\OneToMany]), що не відображено в документації Doctrine.

ЯК ЦЯ СТОРІНКА ВИГЛЯДАЄ В ПОШУКУ
phpukraine.com/docs/symfony/associations
Звʼязки сутностей — Symfony документація українською
Звʼязки сутностей у Symfony 8.1: переклад офіційної документації українською. Оновлено 5 вересня 2026. Приклади коду, пояснення та посилання на питання зі співбесід.
Стан перекладу

Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.

79%
Готовності
5
У роботі
0
Ще не перекладено
Глосарій термінів