📜 ⬆️ ⬇️

Gentlemen's Doctrine 2 for Symfony 3.3.6: Creating Entities, Associations, and Recursive Links



Good day, reader!

What we will do with you in the course of reading the article




Who will be interested in the article:


It will be interesting to the reader if he is already well-versed in symfony - at least one simple project has already been made. I also met interest in this information from advanced developers, so that they can also walk along the diagonal. The article itself is written as an answer to the frequent questions of the guys I work with. Now I can just throw them a link to this article.
')

Create entity


All console commands will be written in a manner as if Composer is not installed in the system.

Install symfony to start:
#             . php composer.phar create-project symfony/framework-standard-edition ./gentlemans_set "v3.3.6" #       cd gentlemans_set/ #     php bin/console doctrine:database:create 


Some people are used to it, but I don’t like to write a lot of code with my own hand. If there is an opportunity to use autogeneration of something in symfony, then I use it and advise you, since it is minimization of the human factor, and just unloads your mind. Let's generate two simple entities via symfony console commands:
 #   User php bin/console doctrine:generate:entity --entity=AppBundle:User --fields="username:string(127) password:string(127)" -q #   Product php bin/console doctrine:generate:entity --entity=AppBundle:Product --fields="name:string(127) description:text" -q 

As a result, we have created two classes of entities:


And two classes of repositories for relevant entities:


Before we proceed to the creation of a database structure for these entities, consider the entities themselves. Namely annotations in them. I believe that most of my readers already understand this topic well, but, nevertheless, I will go through a couple of moments.

Entity src / AppBundle / Entity / Product.php immediately after generation:
 // ... /** * Product * * @ORM\Table(name="product") * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository") */ class Product { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(name="name", type="string", length=127) */ private $name; // ... 


Check what SQL query will be created to create the database structure:
 php bin/console doctrine:schema:create --dump-sql CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(127) NOT NULL, password VARCHAR(127) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(127) NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; 



As a result of the generation of the entity, redundant annotations are created and at this stage the entity will have exactly the same behavior if we reduce the annotations to the variant:

Cleaned version
 // src/AppBundle/Entity/Product.php // ... /** * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository") */ class Product { /** * @var int * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(type="string", length=127) */ private $name; // ... 

Check what SQL query will be created to create the database structure and see exactly the same result:
 php bin/console doctrine:schema:create --dump-sql CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(127) NOT NULL, password VARCHAR(127) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(127) NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; 



What I would like to draw attention to such an example? Overwhelmingly
In most cases, we create the database structure automatically using the entity as a model. I deleted all parts of the annotations, where it was explicitly stated how to name the table for this entity and how to name each of the fields. This configuration data is needed only when we are working with an already existing database and, by some coincidence, the field names do not correspond to the name of the properties in essence. If they are not specified, then Doctrine will name the table according to the name of the entity class, and call the fields according to the name of the entity properties. And this is the right approach, since this is development along the path of least surprise - the names of the fields in the database coincide with the properties of the entity and there is no third party that influences and can “surprise”.

But you will say that we then lose the level of abstraction and with each refactoring of the entity we will have to update the database structure. The answer to this will be: no matter how cool you are, but the uncorrected discrepancy between the names of entity properties and fields in the database as a result of refactoring is an extra point to the technical debt of the project .

I am exaggerating the situation: after a couple of years, when your project has grown to a global scale and you have already smashed the database across a whole cloud of servers, to cope with unbearable workload, you can find in the database a table with the name 'this_may_work' and the fields: 'id' , 'foo', 'bar' and 'some_field_2'. The justification that names have a deeper meaning in the juxtaposed entity will prove to be unimportant.

We start the generation of the database structure:

 php bin/console doctrine:schema:create 


Now we have two entities mapped to the created tables in the database. We can create instances of them, save them in a database, and then fetch them from a database. To demonstrate the creation of instances of entities in this article, I decided to use fixtures, and I will demonstrate the sample in the methods of entity repositories. We already have an entity repository, but we don’t yet have a mechanism for working with fixture data.

Install fixtures



We add the doctrine / doctrine-fixtures-bundle dependency to our project:
 php composer.phar require --dev doctrine/doctrine-fixtures-bundle 


We connect dependency bundle in symfony core:
  // app/AppKernel.php // ... if (in_array($this->getEnvironment(), array('dev', 'test'))) { // ... $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(); } // ... 


Now we are ready to write fixtures. Create a directory for them:

 mkdir -p src/AppBundle/DataFixtures/ORM 


And the initial view of the fixture data:

src / AppBundle / DataFixtures / ORM / LoadCommonData.php
 <?php namespace AppBundle\DataFixtures\ORM; use AppBundle\Entity\Product; use AppBundle\Entity\User; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; class LoadCommonData implements FixtureInterface { public function load(ObjectManager $manager) { $user = new User(); $user ->setPassword('some_password') ->setUsername(''); $manager->persist($user); $product = new Product(); $product ->setName('  ') ->setDescription('   .        .'); $manager->persist($product); $manager->flush(); } } 



Now with this command we can load this fixture data into the database:

 php bin/console doctrine:fixtures:load 


One-to-one bidirectional communication



Above, we got an application with two unrelated entities. What, in fact, makes this application absolutely meaningless. Most data in real-world applications is somehow related to other data. Every your thumbs up, every comment on a social networking page is related to the user's essence, otherwise they are useless.

One-to-one connection - in projects I find it even less often than a many-to-many connection. What is natural, if you follow the normal forms . But to know how it is implemented is necessary.

For example, let's create the Seller entity, which will be connected one-to-one with the User entity, if this user is a seller:

 php bin/console doctrine:generate:entity --entity=AppBundle:Seller --fields="company:string(127) contacts:text" -q 


Then we change the user’s essence to create a connection with the seller’s essence:
src / AppBundle / Entity / User.php
 <?php // ... class User { // ... /** * @ORM\OneToOne(targetEntity="Seller") */ private $seller; // ... } 



We change the essence of the seller by adding a link with the user's essence:
src / AppBundle / Entity / Seller.php
 <?php // ... class Seller { // ... /** * @ORM\OneToOne(targetEntity="User", mappedBy="seller") */ private $user; // ... } 



Note that I immediately gave an example of bidirectional communication ... in general, I see no reason to make unidirectional communication. The database does not change the bidirectional connection, and in the program code provides great convenience. In my opinion, perhaps subjective, even unidirectional communication does not give anything in terms of optimization in speed and use of RAM. (Disclaimer: in the last section of the article, where I talk about recursive communication, I give the only known successful example of unidirectional communication to me.)

Also note that I dropped the annotations again . This time the @JoinColumn annotations. These annotations are needed exactly for the same thing that I also needed the annotations I deleted earlier - to specify with what name a field will be created in the database and for which field a foreign key will be created in the database. All this will be done without this annotation at its best.

We start autogeneration of getter / setter methods in all entities of our bundle. These methods are created for new entity properties:
 php bin/console doctrine:generate:entities AppBundle 


We also need to bring the database structure in line with our entities:
 php bin/console doctrine:schema:update --force 


The last command should be used with caution if there is any data in the database that you do not want to lose. On the production server, this command should not be used at all. Learn on your own what migration database is .

Well and in fixtures we will create one more user who will be already connected with essence of the seller.
src / AppBundle / DataFixtures / ORM / LoadCommonData.php
 <?php namespace AppBundle\DataFixtures\ORM; use AppBundle\Entity\Product; use AppBundle\Entity\Seller; use AppBundle\Entity\User; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; class LoadCommonData implements FixtureInterface { public function load(ObjectManager $manager) { $user = new User(); $user ->setPassword('some_password') ->setUsername(''); $manager->persist($user); $seller = new Seller(); $seller ->setCompany("  ") ->setContacts(" "); $manager->persist($seller); $seller_user = new User(); $seller_user ->setPassword('some_password') ->setUsername('') ->setSeller($seller); $manager->persist($seller_user); $product = new Product(); $product ->setName('  ') ->setDescription('   .        .'); $manager->persist($product); $manager->flush(); } } 



We start loading fixture data and in the database we can admire the records created.

If you have trouble understanding how these annotations have created a connection between entities, and if the official documentation of Doctrine 2 did not help you, then look at the picture:



Here, the arrows indicate where it came from, to be entered in the annotation.

It should be noted that the annotations in both entities turned out to be very similar. Syntactically, they differ only in the mappedBy and inversedBy attributes . But this is a fundamental difference. The entity from which the attribute is inversedBy is usually considered a subordinate entity, which has the attribute mappedBy. It turns out that the User entity is subordinate to the Seller entity. This is expressed in the fact that in the database it is the user table that contains the foreign key on the seller table, and not vice versa. It also affects the code that we wrote in the fixture data - notice that we assigned the seller to the user by the setSeller method, and not the user to the seller. The last option is simply not displayed in any way in the database. That is, we must understand that it is the object of the subordinate entity that is indicated with whom it is associated.

Bidirectional one-to-many communication



The most common form of communication. Therefore, you need to be able to realize it even while intoxicated without the Internet, somewhere on tropical islands being littered with a system of communicating caves without a computer, without hands, without a head. In general, by your own existence, you must carry the one-to-many connection to projects on symfony.

As it was above, we will write an example of a bidirectional connection. To do this, link the already existing entities: Product and Seller.

We answer the question: who will be in this connection a lot, and who will be alone. It usually turns out that a lot of products from one seller. Thus, the new Seller entity property will have the @ORM\OneToMany , and the Product entity properties will have @ORM\ManyToOne . Otherwise, everything is the same as the type of communication "one to one." However, it is no longer possible to swap the mappedBy and inversedBy attributes freely, since the entity from the “many” relationship always submits to the entity from the “one” side. Accordingly, the foreign keys in the database will always be only for products. Continuing the logic: it is the product, as a subordinate entity, that sellers will be assigned by the setSeller method, which we will write below in order for this assignment to be stored in the database.

We change the essence of the seller, to create a connection with the essence of the product:
src / AppBundle / Entity / Seller.php
 <?php // ... class Seller { // ... /** * @ORM\OneToMany(targetEntity="Product", mappedBy="seller") */ private $products; // ... } 



We change the essence of the product by adding a link with the essence of the seller:
src / AppBundle / Entity / Product.php
 <?php // ... class Product { // ... /** * @ORM\ManyToOne(targetEntity="Seller", inversedBy="product") */ private $seller; // ... } 



Run the autogeneration of getter / setter methods again in all entities of our bundle.
Again, run the command to update the database structure. (See above, if you do not remember the command).

We assign the seller to a product that is already being created in our fixture data:

 // src/AppBundle/DataFixtures/ORM/LoadCommonData.php // ... $product = new Product(); $product ->setSeller($seller) ->setName('  ') ->setDescription('   .        .'); // ... 


We start loading fixture data and in the database we can admire the fact that the product record 'Pillow for a programmer' has acquired a value in the field seller_id. This is what we wanted - now in our product each seller can have a lot of goods.

The code to check in any action of any controller:
  // ... $product = $this->getDoctrine() ->getRepository("AppBundle:Product") ->find(6); dump($product->getSeller()->getCompany()); // ... 


6 - this is the id of my product at the time of writing this article (when you start loading fixture data, the old entries in the database tables are deleted, but the auto-increment of the primary key is not reset). Here we got the product from the repository by the value of its primary key (id field), and got the merchant essence associated with it by the getSeller method. At the output we get:
"Horns and hooves"

Now let's try to find all the products of the seller. In the example code for any action of any controller:
  // ... $seller = $this->getDoctrine() ->getRepository("AppBundle:Seller") ->find(5); $names = []; foreach($seller->getProducts() as $product) { $names[] = $product->getName(); } dump($names); // ... 


Output dump function:
array:1 [â–Ľ
0 => " "
]


As for me, the result is achieved.

Bidirectional many-to-many communication



It is very useful to know how such a relationship is organized, although it is much less common one-to-many. I guess you should already know that the many-to-many relationship in the database is organized by linking two tables through a third, pivot table. We don’t need to create an entity for this third table - Doctrine will create this table for us when we run the command to create or update the database structure.

In our growing project, we will create a Category entity and associate its many-to-many with the Product. Thus, we will create the opportunity for a single product to be in several categories at once. The “many-to-many” relationship here is revealed simply: many products may lie in one category, one product may lie in many categories — that from the product side, that from the category side there is a “many” sign, it means that a “many-to-many” link is needed.

Create an entity category:
 php bin/console doctrine:generate:entity --entity=AppBundle:Category --fields="name:string(127)" -q 


Modify the category entity to create a relationship with the product entity:
src / AppBundle / Entity / Category.php
 <?php // ... class Category { // ... /** * @ORM\ManyToMany(targetEntity="Product", mappedBy="categories") */ private $products; // ... } 



We change the essence of the product by adding a link with the category essence:
src / AppBundle / Entity / Product.php
 <?php // ... class Product { // ... /** * @ORM\ManyToMany(targetEntity="Category", inversedBy="products") */ private $categories; // ... } 



We start autogeneration of getter / setter methods in all entities of our bundle. Again, run the command to update the database structure. (See above, if you do not remember the command).

Fixtures are complemented by the creation of two categories, create another product and assign categories to products. It turned out that all products are in all categories. God forgive us God of Marketing, but this was done just for clarity.

src / AppBundle / DataFixtures / ORM / LoadCommonData.php
 <?php namespace AppBundle\DataFixtures\ORM; use AppBundle\Entity\Category; use AppBundle\Entity\Product; use AppBundle\Entity\Seller; use AppBundle\Entity\User; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; class LoadCommonData implements FixtureInterface { public function load(ObjectManager $manager) { $user = new User(); $user ->setPassword('some_password') ->setUsername(''); $manager->persist($user); $seller = new Seller(); $seller ->setCompany("  ") ->setContacts(" "); $manager->persist($seller); $seller_user = new User(); $seller_user ->setPassword('some_password') ->setUsername('') ->setSeller($seller); $manager->persist($seller_user); $category = new Category(); $category->setName(''); $manager->persist($category); $category2 = new Category(); $category2->setName(''); $manager->persist($category2); $product = new Product(); $product ->setSeller($seller) ->setName('  ') ->setDescription('   .        .'); $manager->persist($product); $product2 = new Product(); $product2 ->setSeller($seller) ->setName('  ') ->setDescription(',      64 ,   - 64 ,    16 '); $manager->persist($product2); $product->addCategory($category); $product->addCategory($category2); $product2->addCategory($category); $product2->addCategory($category2); $manager->flush(); } } 



It is important to note here that in spite of the equality of the entities involved in the connection, we still need to use the mappedBy and inversedBy attributes . And this is unexpected, but the behavior that we observed when creating the one-to-many relationship is preserved here - the entity object, on the side of which the mappedBy attribute was specified, should be assigned to the entity object, on the side of which the inversedBy attribute is specified. Otherwise, entries in the pivot table will not appear. It turns out that, willy-nilly, we have to select and keep in mind which of the entities in this connection is subordinate and it is to its objects to assign objects of the second entity. In this case, the subordinate entity - Product and we assign objects to the objects of the Category entity to its objects. If anyone knows how to get around this without crutches, changing only the annotations - write in the comments . I usually get away with a small modification of the setter of the main entity (as I recently discovered, the same solution for the problem is described in the official Doctrine2 documentation):

  // ... /** * Add product * * @param \AppBundle\Entity\Product $product * * @return Category */ public function addProduct(\AppBundle\Entity\Product $product) { $product->addCategory($this); //     ,      ,     $this->products[] = $product; return $this; } // ... 


We start loading the fixture data into the database, check the database and see that the product_category summary table contains 4 records, each of which establishes a connection between a certain category and a specific product:



Recursive communications


A recursive connection is called if it is built with an entity with itself. And this relationship can be one-to-one, one-to-many, and many-to-many. There is where to roam. Let us first consider an option that is not mentioned in the official documentation - a one-to - one recursive “many-to-many” relationship .

We change the user’s essence by adding the many-to-many relationship to itself:
src / AppBundle / Entity / User.php
 <?php // ... class User { // ... /** * @ORM\ManyToMany(targetEntity="User") */ private $friends; // ... } 



This is an example of the successful use of a unidirectional connection, since a comparison of an entity occurs to itself on the primary key using a pivot table - hence, bidirectionality is not required. Run the entity generation for the bundle, update the data structure and you can add our fixtures with strings:

 // ... public function load(ObjectManager $manager) { // ... $user->addFriend($seller_user); $seller_user->addFriend($user); $manager->flush(); } // ... 


Now each user can have as many user-friends as we want.This is a great example of a non-hierarchical structure when there is no master / subordinate, parent / descendant relationship as in a tree structure.

And now we analyze the tree structure by creating a recursive connection. We change the category essence by adding a one-to-many relationship with itself:
src / AppBundle / Entity / Category.php
 <?php // ... class Category { // ... /** * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") */ private $children; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") */ private $parent; // ... } 



I think that everything is so obvious here - the category has a parent and descendants.
After updating the database structure and generating getter / setter methods, add the fixture data with an indication of who is the parent and who is a descendant:

 // ... public function load(ObjectManager $manager) { // ... $category2->setParent($category); $category->addChild($category2); $manager->flush(); } // ... 


I will not analyze the recursive “one-to-one” relationship - if this is required, then using the analogy method, I think, it’s not a problem to write it yourself.
The construction of a hierarchical non-treelike structure, where parents, like descendants, can be more than one, is done using the same analogy method, I think, there is no problem either.

Helpful


Use the symfony toolbar that appears at the bottom of the page when you launch your application in the developer’s environment:



If you mess up with annotations in entities, Doctrine will likely notice this and give you an error here. Click on this icon and be able to read in detail about the error, which should take you out of difficulties.

On the road


He planned to touch upon the topics of polymorphic links with the Signle Table Inheritance, but the article had already grown prohibitively. So leave it all in reserve. Write in the comments, if I got it wrong where, with such a volume of the text, my eyes will wash much.

The project, resulting from the writing of the article, will be posted here: But for those who are just studying the topic of relationships between entities, I advise you to do all the work yourself, and not to take the project ready - this is a good exercise. The article has everything to make a project from scratch.


Source: https://habr.com/ru/post/334446/


All Articles