{"id":345228,"date":"2023-02-09T21:05:13","date_gmt":"2023-02-09T21:05:13","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=345228"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2023-02-09T21:05:13","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=345228","title":{"rendered":"<span>\u041e\u043f\u0442\u0438\u043c\u0438\u0437\u0430\u0446\u0438\u044f OneToMany \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0439 Doctrine<\/span>"},"content":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043a\u0430\u0437\u0430\u043d \u043f\u0440\u0438\u043c\u0435\u0440 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u0443\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043e \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u0435 \u0441 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u044f\u043c\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u0438\u0435\u0440\u0430\u0440\u0445\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u0432 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435 PHP \u0438 Doctrine ORM. \u041f\u043e\u043b\u043d\u044b\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u043c <a href=\"https:\/\/github.com\/franzose\/doctrine-collections-fix-example\" rel=\"noopener noreferrer nofollow\"><u>\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438<\/u><\/a>. \u0421\u0442\u0430\u0442\u044c\u044f \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0440\u0435\u0448\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0438\u0437 \u043e\u0431\u0441\u0443\u0436\u0434\u0435\u043d\u0438\u044f <a href=\"https:\/\/stackoverflow.com\/questions\/13623285\" rel=\"noopener noreferrer nofollow\"><u>\u043d\u0430 StackOverflow<\/u><\/a>.<\/p>\n<p>\u041c\u043d\u043e\u0433\u0438\u0435 \u0438\u0437 \u043d\u0430\u0441 \u0441\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u043b\u0438\u0441\u044c \u0441 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u044c\u044e \u043c\u0430\u043d\u0438\u043f\u0443\u043b\u044f\u0446\u0438\u0438 \u0438\u0435\u0440\u0430\u0440\u0445\u0438\u0447\u0435\u0441\u043a\u0438\u043c\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f\u043c\u0438 \u0442\u043e\u0432\u0430\u0440\u043e\u0432. \u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442 <a href=\"https:\/\/www.slideshare.net\/billkarwin\/models-for-hierarchical-data\" rel=\"noopener noreferrer nofollow\"><u>\u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u044b<\/u><\/a> \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0445 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440 \u0432 \u0411\u0414, \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u0434 \u0442\u0435 \u0438\u043b\u0438 \u0438\u043d\u044b\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f. \u041d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0433\u0434\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0441\u044f \u043a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431: <a href=\"https:\/\/www.programiz.com\/dsa\/graph-adjacency-list\" rel=\"noopener noreferrer nofollow\"><u>adjacency list<\/u><\/a> + <a href=\"https:\/\/dzone.com\/articles\/materialized-paths-tree-structures-relational-database\" rel=\"noopener noreferrer nofollow\"><u>materialized path<\/u><\/a>. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0432 \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u043d\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 adjacency list \u0434\u043b\u044f \u0441\u0432\u044f\u0437\u0435\u0439 \u043c\u0435\u0436\u0434\u0443 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u043c\u0438.<\/p>\n<h2>\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445<\/h2>\n<p>\u0414\u043b\u044f \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043d\u0430\u0448\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e\u0434\u043e\u0439\u0434\u0435\u0442 \u043b\u044e\u0431\u0430\u044f \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f Doctrine DBAL \/ ORM. \u041c\u044b \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f PostgreSQL. \u041f\u0443\u0441\u0442\u044c \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"php\">create sequence public.category_id_seq     increment by 1;  create table if not exists public.category (     id         bigint default nextval('category_id_seq'::regclass) not null primary key,     parent_id  bigint constraint fk_category_id references public.category,     uid        text not null,     name       text not null,     created_at timestamp(0) with time zone not null,     updated_at timestamp(0) with time zone not null );  create unique index uniq_category_uid     on public.category (uid);<\/code><\/pre>\n<p>\u0414\u0440\u0435\u0432\u043e\u0432\u0438\u0434\u043d\u0430\u044f \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0441\u044b\u043b\u043a\u0438 \u0442\u0430\u0431\u043b\u0438\u0446\u044b <code>category<\/code> \u0441\u0430\u043c\u043e\u0439 \u043d\u0430 \u0441\u0435\u0431\u044f \u043f\u043e \u0432\u043d\u0435\u0448\u043d\u0435\u043c\u0443 \u043a\u043b\u044e\u0447\u0443 <code>parent_id<\/code>. \u0412\u043c\u0435\u0441\u0442\u0435 \u0441 \u044d\u0442\u0438\u043c \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044f <code>uid<\/code>, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0440\u043e\u0432\u043d\u0438 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u0431\u0443\u0434\u0443\u0442 \u043e\u0442\u0434\u0435\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0438\u0436\u043d\u0438\u043c \u043f\u043e\u0434\u0447\u0435\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u0435\u043c. \u0422\u043e \u0435\u0441\u0442\u044c, <code>foo_bar<\/code> \u2013 \u044d\u0442\u043e \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f.<\/p>\n<h2>\u0421\u0443\u0449\u043d\u043e\u0441\u0442\u044c<\/h2>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u0434 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438:<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy;  use DateTimeImmutable; use Doctrine\\Common\\Collections\\ArrayCollection; use Doctrine\\Common\\Collections\\Collection; use Doctrine\\ORM\\Mapping\\Column; use Doctrine\\ORM\\Mapping\\Entity; use Doctrine\\ORM\\Mapping\\GeneratedValue; use Doctrine\\ORM\\Mapping\\Id; use Doctrine\\ORM\\Mapping\\JoinColumn; use Doctrine\\ORM\\Mapping\\ManyToOne; use Doctrine\\ORM\\Mapping\\OneToMany; use Doctrine\\ORM\\Mapping\\Table;  #[Entity, Table('category')] class Category {     #[Column(name: 'parent_id', type: 'bigint', nullable: true)]     private ?int $parentId;          #[Column(name: 'uid', type: 'string', nullable: false)]     private string $uid;      #[OneToMany(mappedBy: 'parent', targetEntity: self::class)]     private Collection $children;      #[Column(name: 'created_at', type: 'datetimetz_immutable', nullable: false)]     private DateTimeImmutable $createdAt;      #[Column(name: 'updated_at', type: 'datetimetz_immutable', nullable: false)]     private DateTimeImmutable $updatedAt;      public function __construct(         #[Id]         #[Column(name: 'id', type: 'bigint', nullable: false)]         #[GeneratedValue(strategy: 'NONE')]         private int $id,         string $uid,         #[Column(name: 'name', type: 'string', nullable: false)]         private string $name,         #[ManyToOne(targetEntity: self::class, inversedBy: 'children')]         #[JoinColumn(name: 'parent_id', referencedColumnName: 'id')]         private ?self $parent = null     ) {         $this->parentId = $this->parent?->id;         $this->uid = $this->parent === null ? $uid : sprintf('%s_%s', $this->parent->uid, $uid);         $this->createdAt = new DateTimeImmutable();         $this->updatedAt = new DateTimeImmutable();         $this->children = new ArrayCollection();     }      public function id(): int     {         return $this->id;     }      public function uid(): string     {         return $this->uid;     }      public function name(): string     {         return $this->name;     }      public function parent(): ?self     {         return $this->parent;     }      public function parentId(): ?int     {         return $this->parentId;     }          public function children(): Collection     {         return new ArrayCollection($this->children->toArray());     } }<\/code><\/pre>\n<p>\u041e\u0431\u044b\u0447\u043d\u043e \u0441\u0432\u044f\u0437\u044c <code>ManyToOne<\/code> \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u044e\u0442 \u043e\u0434\u043d\u0438\u043c \u043f\u043e\u043b\u0435\u043c. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, <code>parent<\/code> \u0438\u0437 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043c\u0435\u0440\u0430. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e, \u043f\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u043c \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0438 \u043a \u0442\u0430\u043a\u043e\u043c\u0443 \u043f\u043e\u043b\u044e Doctrine \u0441\u0434\u0435\u043b\u0430\u0435\u0442 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432 \u0411\u0414, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u0443\u044e \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0432 \u043f\u0430\u043c\u044f\u0442\u044c. \u0415\u0441\u043b\u0438 \u0436\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430 \u0436\u0430\u0434\u043d\u0430\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430 <code>fetch: EAGER<\/code>, \u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u0430\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0433\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c\u044e. \u0412 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u044d\u0442\u043e \u043f\u0440\u0438\u0432\u0435\u043b\u043e \u0431\u044b \u043a \u043a\u0430\u0441\u043a\u0430\u0434\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0435\u0432\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c Category \u0441\u0441\u044b\u043b\u0430\u0435\u0442\u0441\u044f \u0441\u0430\u043c\u0430 \u043d\u0430 \u0441\u0435\u0431\u044f. \u0421 \u043c\u043e\u0435\u0439 \u0442\u043e\u0447\u043a\u0438 \u0437\u0440\u0435\u043d\u0438\u044f, \u0434\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u0439 \u044d\u0444\u0444\u0435\u043a\u0442 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u0440\u0430\u0439\u043d\u0435 \u043d\u0435\u0436\u0435\u043b\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438\u0437-\u0437\u0430 \u0440\u0430\u0441\u0445\u043e\u0434\u0430 \u0431\u043e\u043b\u044c\u0448\u043e\u0433\u043e \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0439 \u043f\u0430\u043c\u044f\u0442\u0438 \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0433\u0443\u0442 \u0434\u0430\u0436\u0435 \u043d\u0435 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u044c\u0441\u044f.<\/p>\n<p>\u041f\u043e\u044d\u0442\u043e\u043c\u0443, \u043a\u043e\u0433\u0434\u0430 \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u043e\u0439 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 (\u0432 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u2013 \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0439 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438), \u0430 \u043d\u0435 \u0432\u0441\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0446\u0435\u043b\u0438\u043a\u043e\u043c, \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435 <code>parentId<\/code>. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0432 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0432 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440\u0435, \u0432 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u043c \u043c\u044b \u0432\u0441\u0435\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u043c \u0438\u043c\u0435\u0442\u044c \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0417\u0430\u0442\u0435\u043c, \u043f\u0440\u0438 \u043c\u043d\u043e\u0433\u043e\u043a\u0440\u0430\u0442\u043d\u043e\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044f, \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0411\u0414 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u043d\u0435 \u0431\u0443\u0434\u0435\u0442. \u0414\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043d\u0430\u043c \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0434\u0430\u043b\u0435\u0435 \u043f\u0440\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439.<\/p>\n<h2>\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439<\/h2>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439. \u042d\u0442\u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0438 \u043f\u043e\u0434\u043c\u0435\u043d\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u043f\u0440\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438.<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy;  interface CategoryRepositoryInterface {     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438.      *      * \u0421\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044f \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.      *\/     public function nextCategoryId(): int;      \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u043e\u0442\u043e\u043c\u043a\u0438 \u0432\u0441\u0435\u0445 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0434\u043b\u044f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c UID.      *      * @return Category[]      *\/     public function findDescendantsByUid(string $uid): array; }<\/code><\/pre>\n<p>\u042d\u0442\u0438\u0445 \u0434\u0432\u0443\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u043d\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy;  use Doctrine\\ORM\\EntityManagerInterface;  final readonly class DefaultCategoryRepository implements CategoryRepositoryInterface {     public function __construct(private EntityManagerInterface $em)     {     }      public function nextCategoryId(): int     {         return (int) $this->em             ->getConnection()             ->executeQuery(\"select nextval('category_id_seq')\")             ->fetchOne();     }      \/**      * @return Category[]      *\/     public function findDescendantsByUid(string $uid): array     {         return $this->em             ->createQueryBuilder()             ->select('c')             ->from(Category::class, 'c')             ->where('c.uid LIKE :uid')             ->setParameter('uid', sprintf('%s_%%', $uid))             ->orderBy('c.uid')             ->getQuery()             ->getResult();     } }<\/code><\/pre>\n<p>\u041a\u0440\u0430\u0442\u043a\u043e \u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438. \u041c\u0435\u0442\u043e\u0434 <code>nextCategoryId<\/code> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 PostgreSQL, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0438 \u0432 \u0441\u0430\u043c\u043e\u043c \u043d\u0430\u0447\u0430\u043b\u0435. \u042d\u0442\u0438\u043c \u043c\u0435\u0442\u043e\u0434\u043e\u043c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0432 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 Category \u0438 \u0438\u0445 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0438 \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445. \u041c\u0435\u0442\u043e\u0434 <code>findDescendantsByUid<\/code> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043c\u0430\u0441\u0441\u0438\u0432 \u043f\u043e\u0442\u043e\u043c\u043a\u043e\u0432 \u0432\u0441\u0435\u0445 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0434\u043b\u044f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c UID.<\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/h2>\n<p>\u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0411\u0414 \u0434\u043e \u0438 \u043f\u043e\u0441\u043b\u0435 \u201c\u0444\u0438\u043a\u0441\u0430\u201d, \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 PHPUnit \u0441\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0435\u0439 <code>phpunit.xml<\/code>:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?>  &lt;!-- https:\/\/phpunit.readthedocs.io\/en\/latest\/configuration.html --> &lt;phpunit xmlns:xsi=\"http:\/\/www.w3.org\/2001\/XMLSchema-instance\"          xsi:noNamespaceSchemaLocation=\"vendor\/phpunit\/phpunit\/phpunit.xsd\"          colors=\"true\"          bootstrap=\"vendor\/autoload.php\" >     &lt;php>         &lt;ini name=\"display_errors\" value=\"1\" \/>         &lt;ini name=\"error_reporting\" value=\"-1\" \/>         &lt;server name=\"DB_HOST\" value=\"postgres\" \/>         &lt;server name=\"DB_PORT\" value=\"5432\" \/>         &lt;server name=\"DB_USER\" value=\"user\" \/>         &lt;server name=\"DB_PASSWORD\" value=\"user\" \/>         &lt;server name=\"DB_NAME\" value=\"hierarchy_article_test\" \/>         &lt;server name=\"DB_DRIVER\" value=\"pdo_pgsql\" \/>         &lt;server name=\"DOCTRINE_SRC_DIR\" value=\".\/src\" \/>         &lt;server name=\"DOCTRINE_PROXY_DIR\" value=\".\/proxy\" \/>         &lt;server name=\"DOCTRINE_PROXY_NAMESPACE\" value=\"HierarchyProxy\\\\\" \/>     &lt;\/php>      &lt;testsuites>         &lt;testsuite name=\"Project Test Suite\">             &lt;directory>tests&lt;\/directory>         &lt;\/testsuite>     &lt;\/testsuites> &lt;\/phpunit><\/code><\/pre>\n<p>\u0418\u0442\u0430\u043a, \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u043c \u043a \u043a\u043b\u0430\u0441\u0441\u0443 \u0442\u0435\u0441\u0442\u043e\u0432 \u0438 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043c\u0435\u0442\u043e\u0434 <code>testBehavior<\/code>, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043f\u043e\u0437\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0438\u0441\u0430\u043d\u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  use PHPUnit\\Framework\\TestCase;  final class CategoryRepositoryTest extends TestCase {     public function testBehavior(): void     {         \/\/ \u0437\u0434\u0435\u0441\u044c \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u043f\u0438\u0441\u0430\u043d \u0442\u0435\u0441\u0442     } }<\/code><\/pre>\n<p>\u0410 \u043f\u043e\u043a\u0430 \u0437\u0430\u0439\u043c\u0435\u043c\u0441\u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u043e\u0439 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u043c \u0434\u0435\u0440\u0435\u0432\u043e\u043c \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439.<\/p>\n<pre><code class=\"php\">private static function createCategories(     EntityManagerInterface $em,     CategoryRepositoryInterface $repository ): void {     $parent = new Category($repository->nextCategoryId(), 'foo', 'Foo');     $child = new Category($repository->nextCategoryId(), 'bar', 'Bar', $parent);     $grandChild = new Category($repository->nextCategoryId(), 'qux', 'Qux', $child);     $grandGrandChild = new Category($repository->nextCategoryId(), 'doo', 'Doo', $grandChild);      foreach ([$parent, $child, $grandChild, $grandGrandChild] as $entity) {         $em->persist($entity);     }      $em->flush();     $em->clear(); }<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u044b \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u0438 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439. \u041e\u043f\u0438\u0448\u0435\u043c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0411\u0414 \u0438 \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430.<\/p>\n<pre><code class=\"php\">private static function createEntityManager(Connection $connection): EntityManagerInterface {     $config = new ORMConfiguration();     $config->setMetadataDriverImpl(new AttributeDriver([$_SERVER['DOCTRINE_SRC_DIR']]));     $config->setProxyDir($_SERVER['DOCTRINE_PROXY_DIR']);     $config->setProxyNamespace($_SERVER['DOCTRINE_PROXY_NAMESPACE']);      return new EntityManager($connection, $config); }  private static function createConnection(LoggerInterface $logger): Connection {     $params = [         'host' => $_SERVER['DB_HOST'],         'port' => $_SERVER['DB_PORT'],         'user' => $_SERVER['DB_USER'],         'password' => $_SERVER['DB_PASSWORD'],         'dbname' => $_SERVER['DB_NAME'],         'driver' => $_SERVER['DB_DRIVER']     ];      $config = new DBALConfiguration();     $config->setMiddlewares([         new \\Doctrine\\DBAL\\Logging\\Middleware($logger)     ]);      return DriverManager::getConnection($params, $config); }<\/code><\/pre>\n<p>\u0421\u0443\u0442\u044c \u0442\u0435\u0441\u0442\u0430 \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0435 SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u0442 Doctrine, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0430\u043c \u043d\u0443\u0436\u0435\u043d \u043b\u043e\u0433\u0433\u0435\u0440. \u041d\u0430\u0434\u0435\u044e\u0441\u044c, \u0447\u0442\u043e \u0432\u044b \u0432\u043e\u0432\u0440\u0435\u043c\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u043d\u043e \u0432\u0441\u0451 \u0436\u0435 \u043e\u0431\u0440\u0430\u0449\u0443 \u0432\u0430\u0448\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0411\u0414. \u0412 Doctrine DBAL 3.2.0 <a href=\"https:\/\/github.com\/doctrine\/dbal\/pull\/4967\" rel=\"noopener noreferrer nofollow\"><u>\u043f\u043e\u044f\u0432\u0438\u043b\u0441\u044f<\/u><\/a> \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c middleware, \u0430 \u043c\u0435\u0442\u043e\u0434 <code>Configuration::setSQLLogger()<\/code> \u0438 \u043a\u043b\u0430\u0441\u0441 <code>Doctrine\\DBAL\\Logging\\DebugStack<\/code> \u0431\u044b\u043b\u0438 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u044b \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u043c\u0438 \u0438 \u043d\u0430\u0447\u0430\u043b\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f. \u0422\u043e \u0435\u0441\u0442\u044c, \u0440\u0430\u043d\u044c\u0448\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435:<\/p>\n<pre><code class=\"php\">$debugStack = new DebugStack(); $em->getConnection()->getConfiguration()->setSQLLogger($debugStack); \/\/ \u0434\u0430\u043b\u0435\u0435 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u043c \u0438 \u0438\u0441\u0441\u043b\u0435\u0434\u0443\u0435\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u044b $debugStack->queries;<\/code><\/pre>\n<p>\u041e\u0434\u043d\u0430\u043a\u043e \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c middleware, \u043a\u0430\u043a \u0432 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u0432\u044b\u0448\u0435. \u041f\u0440\u0438\u0447\u0435\u043c \u0430\u043d\u0430\u043b\u043e\u0433\u0430 <code>DebugStack<\/code> \u0432 \u043d\u043e\u0432\u044b\u0445 \u0432\u0435\u0440\u0441\u0438\u044f\u0445 Doctrine DBAL \u043d\u0435\u0442, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0435\u0433\u043e \u043f\u0440\u0438\u0434\u0435\u0442\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u0442\u044c \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e. \u041d\u043e \u044d\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0443\u0436 \u0438 \u0441\u043b\u043e\u0436\u043d\u043e. \u0412\u043e\u0442 \u043f\u0440\u0438\u043c\u0435\u0440 \u0442\u0430\u043a\u043e\u0433\u043e \u043b\u043e\u0433\u0433\u0435\u0440\u0430:<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy\\Tests;  use Psr\\Log\\AbstractLogger; use Stringable;  final class TestDatabaseLogger extends AbstractLogger {     private array $queries = [];          public function log($level, Stringable|string $message, array $context = []): void     {         if (array_key_exists('sql', $context)) {             $this->queries[] = $context;         }     }      \/**      * @return string[]      *\/     public function selectQueries(): array     {         $filter = static fn (string $query) => str_starts_with($query, 'SELECT');                  return array_values(array_filter(array_column($this->queries, 'sql'), $filter));     } }<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0441\u043e\u0431\u0435\u0440\u0435\u043c \u0432\u0441\u0451 \u0432\u043e\u0435\u0434\u0438\u043d\u043e \u0432 \u043c\u0435\u0442\u043e\u0434\u0435 <code>setUpTestData()<\/code>:<\/p>\n<pre><code class=\"php\">#[ArrayShape([TestDatabaseLogger::class, CategoryRepositoryInterface::class])] private static function setUpTestData(string $repositoryClassName): array {     $logger = new TestDatabaseLogger();      $connection = self::createConnection($logger);     $connection->executeStatement('delete from category');      $em = self::createEntityManager($connection);     $repository = new $repositoryClassName($em);      self::createCategories($em, $repository);      return [$logger, $repository]; }<\/code><\/pre>\n<p>\u0411\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044f \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0443 <code>$repositoryClassName<\/code> \u043d\u0430\u043c \u043d\u0435 \u043f\u0440\u0438\u0434\u0451\u0442\u0441\u044f \u201c\u0445\u0430\u0440\u0434\u043a\u043e\u0434\u0438\u0442\u044c\u201d \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f, \u0438 \u043c\u044b \u0441\u043c\u043e\u0436\u0435\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u043e\u0440\u043a\u0438, \u201c\u043e\u0431\u044b\u0447\u043d\u0443\u044e\u201d \u0438 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0443\u044e, \u043d\u0435 \u043c\u0435\u043d\u044f\u044f \u043f\u0440\u0438 \u043d\u0438 \u0442\u0435\u0441\u0442, \u043d\u0438 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434. \u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0441\u0430\u043c \u0442\u0435\u0441\u0442:<\/p>\n<pre><code class=\"php\">\/**  * @dataProvider behaviourDataProvider  *\/ public function testBehavior(string $repositoryClassName, array $expectedQueries): void {     [$logger, $repository] = self::setUpTestData($repositoryClassName);      \/\/ foo_bar, foo_bar_qux, foo_bar_qux_doo     $descendants = $repository->findDescendantsByUid('foo');          self::assertEquals('foo_bar_qux_doo', $descendants[0]->children()->first()->children()->first()->uid());     self::assertEquals($expectedQueries, $logger->selectQueries()); }  public function behaviourDataProvider(): array {     return [         [             DefaultCategoryRepository::class,             [                 'SELECT c0_.parent_id AS parent_id_0, c0_.uid AS uid_1, c0_.created_at AS created_at_2, c0_.updated_at AS updated_at_3, c0_.id AS id_4, c0_.name AS name_5, c0_.parent_id AS parent_id_6 FROM category c0_ WHERE c0_.uid LIKE ? ORDER BY c0_.uid ASC',                 'SELECT t0.parent_id AS parent_id_1, t0.uid AS uid_2, t0.created_at AS created_at_3, t0.updated_at AS updated_at_4, t0.id AS id_5, t0.name AS name_6, t0.parent_id AS parent_id_7 FROM category t0 WHERE t0.parent_id = ?',                 'SELECT t0.parent_id AS parent_id_1, t0.uid AS uid_2, t0.created_at AS created_at_3, t0.updated_at AS updated_at_4, t0.id AS id_5, t0.name AS name_6, t0.parent_id AS parent_id_7 FROM category t0 WHERE t0.parent_id = ?'             ]         ]     ]; }<\/code><\/pre>\n<p>\u0418 \u0443\u0431\u0435\u0434\u0438\u043c\u0441\u044f, \u0447\u0442\u043e \u043e\u043d \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0443\u0441\u043f\u0435\u0448\u043d\u043e:<\/p>\n<pre><code class=\"bash\">PHPUnit 9.5.27 by Sebastian Bergmann and contributors.  .                    1 \/ 1 (100%)  Time: 00:00.093, Memory: 10.00 MB  OK (1 test, 2 assertions) Process finished with exit code 0 <\/code><\/pre>\n<p>\u0421\u0443\u0442\u044c \u0442\u0435\u0441\u0442\u0430 \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c:<\/p>\n<ol>\n<li>\n<p>\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0432\u0441\u0435\u0445 \u043f\u043e\u0442\u043e\u043c\u043a\u043e\u0432 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 <code>foo<\/code><\/p>\n<\/li>\n<li>\n<p>\u0412\u0437\u044f\u0442\u044c \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c <code>foo_bar<\/code>, \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0440\u0430\u0437 \u043f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0432\u0433\u043b\u0443\u0431\u044c \u0434\u0435\u0440\u0435\u0432\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0432\u044b\u0437\u043e\u0432\u0430 \u043c\u0435\u0442\u043e\u0434\u0430 <code>children()<\/code> \u0438 \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u043a\u0430\u0436\u0434\u044b\u0439 \u0442\u0430\u043a\u043e\u0439 \u0432\u044b\u0437\u043e\u0432 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u0442 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441 \u0432 \u0411\u0414<\/p>\n<\/li>\n<\/ol>\n<p>\u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e Doctrine \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 OneToMany-\u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u044e \u0438\u0437 \u0411\u0414 \u0432\u0441\u044f\u043a\u0438\u0439 \u0440\u0430\u0437, \u043a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u043c\u0443 \u043f\u043e\u043b\u044e. \u0414\u0430\u0436\u0435 \u043d\u0435 \u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u044d\u0442\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u0443\u0436\u0435 \u0431\u044b\u043b\u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u044b \u0432 \u043f\u0430\u043c\u044f\u0442\u044c. \u041a\u0430\u043a \u0440\u0430\u0437 \u044d\u0442\u043e \u043d\u0430\u043c \u0438 \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u0438\u0442 \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.<\/p>\n<h2>\u041e\u043f\u0442\u0438\u043c\u0438\u0437\u0430\u0446\u0438\u044f<\/h2>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 <code>ImprovedCategoryRepository<\/code> \u0441\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u044b\u043c:<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy;  use Doctrine\\ORM\\EntityManagerInterface; use Doctrine\\ORM\\PersistentCollection;  final readonly class ImprovedCategoryRepository implements CategoryRepositoryInterface {     public function __construct(private EntityManagerInterface $em)     {     }      public function nextCategoryId(): int     {         return (int) $this->em             ->getConnection()             ->executeQuery(\"select nextval('category_id_seq')\")             ->fetchOne();     }      \/**      * @return Category[]      *\/     public function findDescendantsByUid(string $uid): array     {         $result = $this->em             ->createQueryBuilder()             ->select('c')             ->from(Category::class, 'c')             ->indexBy('c', 'c.id')             ->where('c.uid LIKE :uid')             ->setParameter('uid', sprintf('%s_%%', $uid))             ->orderBy('c.uid')             ->getQuery()             ->getResult();          return $this->setUpCategoryRelations($result);     }      \/**      * @param Category[] $categories      * @return Category[]      *\/     private function setUpCategoryRelations(array $categories): array     {         $metadata = $this->em->getClassMetadata(Category::class);         $idField = $metadata->reflFields['id'];         $parentField = $metadata->reflFields['parent'];         $parentIdField = $metadata->reflFields['parentId'];         $childrenField = $metadata->reflFields['children'];          foreach ($categories as $category) {             \/** @var PersistentCollection $children *\/             $children = $childrenField->getValue($category);             $children->setInitialized(true);              $parent = $categories[$parentIdField->getValue($category)] ?? null;              if ($parent === null) {                 continue;             }              \/** @var PersistentCollection $children *\/             $children = $childrenField->getValue($parent);              if (!$children->contains($category)) {                 $parentField->setValue($category, $parent);                 $parentIdField->setValue($category, $idField->getValue($parent));                 $children->add($category);             }         }          return array_values($categories);     } }<\/code><\/pre>\n<p>DQL \u0432\u044b\u0431\u043e\u0440\u043a\u0438 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043b\u0438\u0448\u044c \u0442\u0435\u043c, \u0447\u0442\u043e \u043c\u044b \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043d\u0434\u0435\u043a\u0441\u0430 \u043c\u0430\u0441\u0441\u0438\u0432\u0430. \u042d\u0442\u043e \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0434\u0430\u043b\u0435\u0435 \u043f\u0440\u0438 \u043e\u0431\u0445\u043e\u0434\u0435 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439:<\/p>\n<pre><code class=\"php\">$this->em      ->createQueryBuilder()      ->select('c')      ->from(Category::class, 'c')      ->indexBy('c', 'c.id')<\/code><\/pre>\n<p>\u0412\u0441\u0451 \u0441\u0430\u043c\u043e\u0435 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e\u0435 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0434\u0430\u043b\u0435\u0435 \u0432 \u043c\u0435\u0442\u043e\u0434\u0435 <code>setUpCategoryRelations()<\/code>. \u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0447\u0435\u0440\u0435\u0437 \u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0435 \u043c\u044b \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u043f\u043e\u043b\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 Category, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438 \u0431\u0443\u0434\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c.<\/p>\n<pre><code class=\"php\">$metadata = $this->em->getClassMetadata(Category::class); $idField = $metadata->reflFields['id']; $parentField = $metadata->reflFields['parent']; $parentIdField = $metadata->reflFields['parentId']; $childrenField = $metadata->reflFields['children'];<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u044b \u201c\u043e\u0431\u043c\u0430\u043d\u0435\u043c\u201d Doctrine \u0438 \u0443\u043a\u0430\u0436\u0435\u043c, \u0447\u0442\u043e \u0432\u0441\u0435 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0445 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u0443\u0436\u0435 \u0431\u044b\u043b\u0438 \u043f\u0440\u043e\u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u044b:<\/p>\n<pre><code class=\"php\">\/** @var PersistentCollection $children *\/ $children = $childrenField->getValue($category); $children->setInitialized(true);<\/code><\/pre>\n<p>\u0422\u043e\u0433\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u044f\u0445 \u043a \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0443 <code>Category::$children<\/code> Doctrine \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432 \u0411\u0414! \u041d\u043e \u044d\u0442\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u0442\u044c \u043d\u0430\u0448\u0435\u0433\u043e \u201c\u0444\u0438\u043a\u0441\u0430\u201d, \u0432\u0435\u0434\u044c \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u043f\u0443\u0441\u0442\u044b\u043c\u0438. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0441\u043e\u0431\u0435\u0440\u0435\u043c \u0438\u0445 \u0432\u0440\u0443\u0447\u043d\u0443\u044e:<\/p>\n<pre><code class=\"php\">$parent = $categories[$parentIdField->getValue($category)] ?? null;  if ($parent === null) {     continue; }  \/** @var PersistentCollection $children *\/ $children = $childrenField->getValue($parent);  if (!$children->contains($category)) {     $parentField->setValue($category, $parent);     $parentIdField->setValue($category, $idField->getValue($parent));     $children->add($category);<\/code><\/pre>\n<p>\u0418\u0437-\u0437\u0430 \u0440\u0435\u0444\u043b\u0435\u043a\u0441\u0438\u0438 \u043a\u043e\u0434 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u0442\u0440\u0430\u0448\u043d\u043e\u0432\u0430\u0442\u043e, \u043d\u043e \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0438\u043c\u0435\u0435\u0442 \u043f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432\u0430:<\/p>\n<ol>\n<li>\n<p>\u0422\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043c\u0430\u043d\u0438\u043f\u0443\u043b\u044f\u0446\u0438\u0438 \u043d\u0430\u0434 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u043e\u0442\u0434\u0435\u043b\u0435\u043d\u044b \u043e\u0442 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u041a\u0430\u043a\u043e\u0435 \u0431\u044b \u0442\u043e \u043d\u0438 \u0431\u044b\u043b\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u043d\u0435 \u0437\u0430\u0442\u0440\u043e\u043d\u0435\u0442 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u0438 \u043d\u0435 \u0441\u043b\u043e\u043c\u0430\u0435\u0442 \u0435\u0433\u043e<\/p>\n<\/li>\n<\/ol>\n<p>\u041f\u043e \u0444\u0430\u043a\u0442\u0443 \u043a\u043e\u0434 \u0432\u044b\u0448\u0435 \u0434\u0435\u043b\u0430\u0435\u0442 \u0442\u043e \u0436\u0435 \u0441\u0430\u043c\u043e\u0435, \u043a\u0430\u043a \u0435\u0441\u043b\u0438 \u0431\u044b \u0432 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u043c\u044b \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0442\u0430\u043a\u043e\u0439 \u043c\u0435\u0442\u043e\u0434 \u0438 \u0437\u0430\u0442\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0438 \u0435\u0433\u043e \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438:<\/p>\n<pre><code class=\"php\">public function addChild(self $child): void {     if (!$this->children->contains($child)) {         $child->parent = $this;         $child->parentId = $this->id;         $this->children->add($child);     } }<\/code><\/pre>\n<h2>\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/h2>\n<p>\u0414\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 <code>CategoryRepositoryInterface<\/code> \u043d\u0430\u043c \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432\u0442\u043e\u0440\u043e\u0439 \u043d\u0430\u0431\u043e\u0440 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 data provider\u00a0 <code>CategoryRepositoryTest::behaviorDataProvider()<\/code>:<\/p>\n<pre><code class=\"php\">public function behaviourDataProvider(): array {     return [         [             DefaultCategoryRepository::class,             [                 'SELECT c0_.parent_id AS parent_id_0, c0_.uid AS uid_1, c0_.created_at AS created_at_2, c0_.updated_at AS updated_at_3, c0_.id AS id_4, c0_.name AS name_5, c0_.parent_id AS parent_id_6 FROM category c0_ WHERE c0_.uid LIKE ? ORDER BY c0_.uid ASC',                 'SELECT t0.parent_id AS parent_id_1, t0.uid AS uid_2, t0.created_at AS created_at_3, t0.updated_at AS updated_at_4, t0.id AS id_5, t0.name AS name_6, t0.parent_id AS parent_id_7 FROM category t0 WHERE t0.parent_id = ?',                 'SELECT t0.parent_id AS parent_id_1, t0.uid AS uid_2, t0.created_at AS created_at_3, t0.updated_at AS updated_at_4, t0.id AS id_5, t0.name AS name_6, t0.parent_id AS parent_id_7 FROM category t0 WHERE t0.parent_id = ?'             ]         ],         [             ImprovedCategoryRepository::class,             [                 'SELECT c0_.parent_id AS parent_id_0, c0_.uid AS uid_1, c0_.created_at AS created_at_2, c0_.updated_at AS updated_at_3, c0_.id AS id_4, c0_.name AS name_5, c0_.parent_id AS parent_id_6 FROM category c0_ WHERE c0_.uid LIKE ? ORDER BY c0_.uid ASC',             ]         ]     ]; }<\/code><\/pre>\n<p>\u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u043c\u044b \u043e\u0436\u0438\u0434\u0430\u0435\u043c, \u0447\u0442\u043e \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0435\u0442\u043e\u0434\u0430 <code>ImprovedCategoryRepository::findDescendantsByUid()<\/code> \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0438\u0435 \u043c\u0430\u043d\u0438\u043f\u0443\u043b\u044f\u0446\u0438\u0438 \u043d\u0430\u0434 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u043c\u0438 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u044f\u043c\u0438 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0432 \u0411\u0414. \u0423\u0431\u0435\u0434\u0438\u043c\u0441\u044f \u0432 \u044d\u0442\u043e\u043c, \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0432 \u0442\u0435\u0441\u0442:<\/p>\n<pre><code class=\"bash\">PHPUnit 9.5.27 by Sebastian Bergmann and contributors.  ..                   2 \/ 2 (100%)  Time: 00:00.100, Memory: 10.00 MB  OK (2 tests, 4 assertions) Process finished with exit code 0 <\/code><\/pre>\n<p>\u0412\u0443\u0430\u043b\u044f, \u043d\u0430\u0448 \u201c\u0444\u0438\u043a\u0441\u201d \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b! \u041d\u0430 \u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u0434\u0435\u0440\u0435\u0432\u044c\u044f\u0445 \u044d\u0444\u0444\u0435\u043a\u0442 \u0431\u0443\u0434\u0435\u0442 \u043a\u0443\u0434\u0430 \u0437\u0430\u043c\u0435\u0442\u043d\u0435\u0435, \u0442.\u043a. \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u043e\u0442\u0435\u043d \u0438\u043b\u0438 \u0442\u044b\u0441\u044f\u0447 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f \u043b\u0438\u0448\u044c \u043e\u0434\u0438\u043d: \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e \u043d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435. \u041d\u0430\u0434\u0435\u044e\u0441\u044c, \u0447\u0442\u043e \u0441\u0442\u0430\u0442\u044c\u044f \u0431\u044b\u043b\u0430 \u043f\u043e\u043b\u0435\u0437\u043d\u043e\u0439 \u0434\u043b\u044f \u0432\u0430\u0441.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p> <!----> <!----><\/div>\n<p> <!----> <!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/715942\/\"> https:\/\/habr.com\/ru\/post\/715942\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043a\u0430\u0437\u0430\u043d \u043f\u0440\u0438\u043c\u0435\u0440 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u0443\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043e \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u0435 \u0441 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u044f\u043c\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u0438\u0435\u0440\u0430\u0440\u0445\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u0432 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435 PHP \u0438 Doctrine ORM. \u041f\u043e\u043b\u043d\u044b\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u043c <a href=\"https:\/\/github.com\/franzose\/doctrine-collections-fix-example\" rel=\"noopener noreferrer nofollow\"><u>\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438<\/u><\/a>. \u0421\u0442\u0430\u0442\u044c\u044f \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0440\u0435\u0448\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0438\u0437 \u043e\u0431\u0441\u0443\u0436\u0434\u0435\u043d\u0438\u044f <a href=\"https:\/\/stackoverflow.com\/questions\/13623285\" rel=\"noopener noreferrer nofollow\"><u>\u043d\u0430 StackOverflow<\/u><\/a>.<\/p>\n<p>\u041c\u043d\u043e\u0433\u0438\u0435 \u0438\u0437 \u043d\u0430\u0441 \u0441\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u043b\u0438\u0441\u044c \u0441 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u044c\u044e \u043c\u0430\u043d\u0438\u043f\u0443\u043b\u044f\u0446\u0438\u0438 \u0438\u0435\u0440\u0430\u0440\u0445\u0438\u0447\u0435\u0441\u043a\u0438\u043c\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f\u043c\u0438 \u0442\u043e\u0432\u0430\u0440\u043e\u0432. \u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442 <a href=\"https:\/\/www.slideshare.net\/billkarwin\/models-for-hierarchical-data\" rel=\"noopener noreferrer nofollow\"><u>\u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u044b<\/u><\/a> \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0445 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440 \u0432 \u0411\u0414, \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u0434 \u0442\u0435 \u0438\u043b\u0438 \u0438\u043d\u044b\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f. \u041d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0433\u0434\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0441\u044f \u043a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431: <a href=\"https:\/\/www.programiz.com\/dsa\/graph-adjacency-list\" rel=\"noopener noreferrer nofollow\"><u>adjacency list<\/u><\/a> + <a href=\"https:\/\/dzone.com\/articles\/materialized-paths-tree-structures-relational-database\" rel=\"noopener noreferrer nofollow\"><u>materialized path<\/u><\/a>. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0432 \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u043d\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 adjacency list \u0434\u043b\u044f \u0441\u0432\u044f\u0437\u0435\u0439 \u043c\u0435\u0436\u0434\u0443 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u043c\u0438.<\/p>\n<h2>\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445<\/h2>\n<p>\u0414\u043b\u044f \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043d\u0430\u0448\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e\u0434\u043e\u0439\u0434\u0435\u0442 \u043b\u044e\u0431\u0430\u044f \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f Doctrine DBAL \/ ORM. \u041c\u044b \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f PostgreSQL. \u041f\u0443\u0441\u0442\u044c \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"php\">create sequence public.category_id_seq     increment by 1;  create table if not exists public.category (     id         bigint default nextval('category_id_seq'::regclass) not null primary key,     parent_id  bigint constraint fk_category_id references public.category,     uid        text not null,     name       text not null,     created_at timestamp(0) with time zone not null,     updated_at timestamp(0) with time zone not null );  create unique index uniq_category_uid     on public.category (uid);<\/code><\/pre>\n<p>\u0414\u0440\u0435\u0432\u043e\u0432\u0438\u0434\u043d\u0430\u044f \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0441\u044b\u043b\u043a\u0438 \u0442\u0430\u0431\u043b\u0438\u0446\u044b <code>category<\/code> \u0441\u0430\u043c\u043e\u0439 \u043d\u0430 \u0441\u0435\u0431\u044f \u043f\u043e \u0432\u043d\u0435\u0448\u043d\u0435\u043c\u0443 \u043a\u043b\u044e\u0447\u0443 <code>parent_id<\/code>. \u0412\u043c\u0435\u0441\u0442\u0435 \u0441 \u044d\u0442\u0438\u043c \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044f <code>uid<\/code>, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0440\u043e\u0432\u043d\u0438 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u0431\u0443\u0434\u0443\u0442 \u043e\u0442\u0434\u0435\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0438\u0436\u043d\u0438\u043c \u043f\u043e\u0434\u0447\u0435\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u0435\u043c. \u0422\u043e \u0435\u0441\u0442\u044c, <code>foo_bar<\/code> \u2013 \u044d\u0442\u043e \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f.<\/p>\n<h2>\u0421\u0443\u0449\u043d\u043e\u0441\u0442\u044c<\/h2>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u0434 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438:<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy;  use DateTimeImmutable; use Doctrine\\Common\\Collections\\ArrayCollection; use Doctrine\\Common\\Collections\\Collection; use Doctrine\\ORM\\Mapping\\Column; use Doctrine\\ORM\\Mapping\\Entity; use Doctrine\\ORM\\Mapping\\GeneratedValue; use Doctrine\\ORM\\Mapping\\Id; use Doctrine\\ORM\\Mapping\\JoinColumn; use Doctrine\\ORM\\Mapping\\ManyToOne; use Doctrine\\ORM\\Mapping\\OneToMany; use Doctrine\\ORM\\Mapping\\Table;  #[Entity, Table('category')] class Category {     #[Column(name: 'parent_id', type: 'bigint', nullable: true)]     private ?int $parentId;          #[Column(name: 'uid', type: 'string', nullable: false)]     private string $uid;      #[OneToMany(mappedBy: 'parent', targetEntity: self::class)]     private Collection $children;      #[Column(name: 'created_at', type: 'datetimetz_immutable', nullable: false)]     private DateTimeImmutable $createdAt;      #[Column(name: 'updated_at', type: 'datetimetz_immutable', nullable: false)]     private DateTimeImmutable $updatedAt;      public function __construct(         #[Id]         #[Column(name: 'id', type: 'bigint', nullable: false)]         #[GeneratedValue(strategy: 'NONE')]         private int $id,         string $uid,         #[Column(name: 'name', type: 'string', nullable: false)]         private string $name,         #[ManyToOne(targetEntity: self::class, inversedBy: 'children')]         #[JoinColumn(name: 'parent_id', referencedColumnName: 'id')]         private ?self $parent = null     ) {         $this->parentId = $this->parent?->id;         $this->uid = $this->parent === null ? $uid : sprintf('%s_%s', $this->parent->uid, $uid);         $this->createdAt = new DateTimeImmutable();         $this->updatedAt = new DateTimeImmutable();         $this->children = new ArrayCollection();     }      public function id(): int     {         return $this->id;     }      public function uid(): string     {         return $this->uid;     }      public function name(): string     {         return $this->name;     }      public function parent(): ?self     {         return $this->parent;     }      public function parentId(): ?int     {         return $this->parentId;     }          public function children(): Collection     {         return new ArrayCollection($this->children->toArray());     } }<\/code><\/pre>\n<p>\u041e\u0431\u044b\u0447\u043d\u043e \u0441\u0432\u044f\u0437\u044c <code>ManyToOne<\/code> \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u044e\u0442 \u043e\u0434\u043d\u0438\u043c \u043f\u043e\u043b\u0435\u043c. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, <code>parent<\/code> \u0438\u0437 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043c\u0435\u0440\u0430. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e, \u043f\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u043c \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0438 \u043a \u0442\u0430\u043a\u043e\u043c\u0443 \u043f\u043e\u043b\u044e Doctrine \u0441\u0434\u0435\u043b\u0430\u0435\u0442 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432 \u0411\u0414, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u0443\u044e \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0432 \u043f\u0430\u043c\u044f\u0442\u044c. \u0415\u0441\u043b\u0438 \u0436\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430 \u0436\u0430\u0434\u043d\u0430\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430 <code>fetch: EAGER<\/code>, \u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u0430\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0433\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c\u044e. \u0412 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u044d\u0442\u043e \u043f\u0440\u0438\u0432\u0435\u043b\u043e \u0431\u044b \u043a \u043a\u0430\u0441\u043a\u0430\u0434\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0435\u0432\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c Category \u0441\u0441\u044b\u043b\u0430\u0435\u0442\u0441\u044f \u0441\u0430\u043c\u0430 \u043d\u0430 \u0441\u0435\u0431\u044f. \u0421 \u043c\u043e\u0435\u0439 \u0442\u043e\u0447\u043a\u0438 \u0437\u0440\u0435\u043d\u0438\u044f, \u0434\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u0439 \u044d\u0444\u0444\u0435\u043a\u0442 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u0440\u0430\u0439\u043d\u0435 \u043d\u0435\u0436\u0435\u043b\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438\u0437-\u0437\u0430 \u0440\u0430\u0441\u0445\u043e\u0434\u0430 \u0431\u043e\u043b\u044c\u0448\u043e\u0433\u043e \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0439 \u043f\u0430\u043c\u044f\u0442\u0438 \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0433\u0443\u0442 \u0434\u0430\u0436\u0435 \u043d\u0435 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u044c\u0441\u044f.<\/p>\n<p>\u041f\u043e\u044d\u0442\u043e\u043c\u0443, \u043a\u043e\u0433\u0434\u0430 \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u043e\u0439 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 (\u0432 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u2013 \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0439 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438), \u0430 \u043d\u0435 \u0432\u0441\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0446\u0435\u043b\u0438\u043a\u043e\u043c, \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435 <code>parentId<\/code>. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0432 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0432 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440\u0435, \u0432 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u043c \u043c\u044b \u0432\u0441\u0435\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u043c \u0438\u043c\u0435\u0442\u044c \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0417\u0430\u0442\u0435\u043c, \u043f\u0440\u0438 \u043c\u043d\u043e\u0433\u043e\u043a\u0440\u0430\u0442\u043d\u043e\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044f, \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0411\u0414 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u043d\u0435 \u0431\u0443\u0434\u0435\u0442. \u0414\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043d\u0430\u043c \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0434\u0430\u043b\u0435\u0435 \u043f\u0440\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439.<\/p>\n<h2>\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439<\/h2>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439. \u042d\u0442\u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0438 \u043f\u043e\u0434\u043c\u0435\u043d\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u043f\u0440\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438.<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy;  interface CategoryRepositoryInterface {     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438.      *      * \u0421\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044f \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.      *\/     public function nextCategoryId(): int;      \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u043e\u0442\u043e\u043c\u043a\u0438 \u0432\u0441\u0435\u0445 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0434\u043b\u044f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c UID.      *      * @return Category[]      *\/     public function findDescendantsByUid(string $uid): array; }<\/code><\/pre>\n<p>\u042d\u0442\u0438\u0445 \u0434\u0432\u0443\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u043d\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  namespace Hierarchy;  use Doctrine\\ORM\\EntityManagerInterface;  final readonly class DefaultCategoryRepository implements CategoryRepositoryInterface {     public function __construct(private EntityManagerInterface $em)     {     }      public function nextCategoryId(): int     {         return (int) $this->em             ->getConnection()             ->executeQuery(\"select nextval('category_id_seq')\")             ->fetchOne();     }      \/**      * @return Category[]      *\/     public function findDescendantsByUid(string $uid): array     {         return $this->em             ->createQueryBuilder()             ->select('c')             ->from(Category::class, 'c')             ->where('c.uid LIKE :uid')             ->setParameter('uid', sprintf('%s_%%', $uid))             ->orderBy('c.uid')             ->getQuery()             ->getResult();     } }<\/code><\/pre>\n<p>\u041a\u0440\u0430\u0442\u043a\u043e \u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438. \u041c\u0435\u0442\u043e\u0434 <code>nextCategoryId<\/code> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 PostgreSQL, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0438 \u0432 \u0441\u0430\u043c\u043e\u043c \u043d\u0430\u0447\u0430\u043b\u0435. \u042d\u0442\u0438\u043c \u043c\u0435\u0442\u043e\u0434\u043e\u043c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0432 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 Category \u0438 \u0438\u0445 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0438 \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445. \u041c\u0435\u0442\u043e\u0434 <code>findDescendantsByUid<\/code> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043c\u0430\u0441\u0441\u0438\u0432 \u043f\u043e\u0442\u043e\u043c\u043a\u043e\u0432 \u0432\u0441\u0435\u0445 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0434\u043b\u044f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c UID.<\/p>\n<h2>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/h2>\n<p>\u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0411\u0414 \u0434\u043e \u0438 \u043f\u043e\u0441\u043b\u0435 \u201c\u0444\u0438\u043a\u0441\u0430\u201d, \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 PHPUnit \u0441\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0435\u0439 <code>phpunit.xml<\/code>:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?>  &lt;!-- https:\/\/phpunit.readthedocs.io\/en\/latest\/configuration.html --> &lt;phpunit xmlns:xsi=\"http:\/\/www.w3.org\/2001\/XMLSchema-instance\"          xsi:noNamespaceSchemaLocation=\"vendor\/phpunit\/phpunit\/phpunit.xsd\"          colors=\"true\"          bootstrap=\"vendor\/autoload.php\" >     &lt;php>         &lt;ini name=\"display_errors\" value=\"1\" \/>         &lt;ini name=\"error_reporting\" value=\"-1\" \/>         &lt;server name=\"DB_HOST\" value=\"postgres\" \/>         &lt;server name=\"DB_PORT\" value=\"5432\" \/>         &lt;server name=\"DB_USER\" value=\"user\" \/>         &lt;server name=\"DB_PASSWORD\" value=\"user\" \/>         &lt;server name=\"DB_NAME\" value=\"hierarchy_article_test\" \/>         &lt;server name=\"DB_DRIVER\" value=\"pdo_pgsql\" \/>         &lt;server name=\"DOCTRINE_SRC_DIR\" value=\".\/src\" \/>         &lt;server name=\"DOCTRINE_PROXY_DIR\" value=\".\/proxy\" \/>         &lt;server name=\"DOCTRINE_PROXY_NAMESPACE\" value=\"HierarchyProxy\\\\\" \/>     &lt;\/php>      &lt;testsuites>         &lt;testsuite name=\"Project Test Suite\">             &lt;directory>tests&lt;\/directory>         &lt;\/testsuite>     &lt;\/testsuites> &lt;\/phpunit><\/code><\/pre>\n<p>\u0418\u0442\u0430\u043a, \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u043c \u043a \u043a\u043b\u0430\u0441\u0441\u0443 \u0442\u0435\u0441\u0442\u043e\u0432 \u0438 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043c\u0435\u0442\u043e\u0434 <code>testBehavior<\/code>, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043f\u043e\u0437\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0438\u0441\u0430\u043d\u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.<\/p>\n<pre><code class=\"php\">&lt;?php declare(strict_types=1);  use PHPUnit\\Framework\\TestCase;  final class CategoryRepositoryTest extends TestCase {     public function testBehavior(): void     {         \/\/ \u0437\u0434\u0435\u0441\u044c \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u043f\u0438\u0441\u0430\u043d \u0442\u0435\u0441\u0442     } }<\/code><\/pre>\n<p>\u0410 \u043f\u043e\u043a\u0430 \u0437\u0430\u0439\u043c\u0435\u043c\u0441\u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u043e\u0439 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u043c \u0434\u0435\u0440\u0435\u0432\u043e\u043c \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439.<\/p>\n<pre><code class=\"php\">private static function createCategories(     EntityManagerInterface $em,     CategoryRepositoryInterface $repository ): void {     $parent = new Category($repository->nextCategoryId(), 'foo', 'Foo');     $child = new Category($repository->nextCategoryId(), 'bar', 'Bar', $parent);     $grandChild = new Category($repository->nextCategoryId(), 'qux', 'Qux', $child);     $grandGrandChild = new Category($repository->nextCategoryId(), 'doo', 'Doo', $grandChild);      foreach ([$parent, $child, $grandChild, $grandGrandChild] as $entity) {         $em->persist($entity);     }      $em->flush();     $em->clear(); }<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u044b \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 \u0438 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0439. \u041e\u043f\u0438\u0448\u0435\u043c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0411\u0414 \u0438 \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430.<\/p>\n<pre><code class=\"php\">private static function createEntityManager(Connection $connection): EntityManagerInterface {     $config = new ORMConfiguration();     $config->setMetadataDriverImpl(new AttributeDriver([$_SERVER['DOCTRINE_SRC_DIR']]));<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-345228","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/345228","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=345228"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/345228\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=345228"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=345228"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=345228"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}