2

I'm trying to learn Symfony. Today I was following The associations tutorial. I decided to make a small application that a House, Kitchens, Bedrooms, and cabinets. I (tried to ;-) ) make a small Class diagram using draw.io to give you a better idea.

This is it

So basically a House can have multiple Bedrooms and multiple Kitchens. Each kitchen can have multiple cabinets. The House has an id and a name. The Bedroom and Kitchen as well. The cabinet has id, shopUrl and is also linked via a foreign key (account_id) to its parent Kitchen. I also link the Kitchen and the Bedroom to the House using a foreign key (house_id). So I followed the tutorial and created the House entity:

    <?php

    namespace App\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Collection;

    /**
     * @ORM\Entity(repositoryClass="App\Repository\HouseRepository")
     */
    class House implements \JsonSerializable
    {

        /**
         * @ORM\OneToMany(targetEntity="App\Entity\Kitchen", mappedBy="house")
         */
        private $kitchen;

        /**
         * @ORM\OneToMany(targetEntity="App\Entity\Bedroom", mappedBy="house")
         */
        private $bedroom;    

        /**
         * House constructor
         */
        public function __construct()
        {
            $this->kitchen = new ArrayCollection();
            $this->bedroom = new ArrayCollection();
        }

        /**
         * @return Collection|Kitchen[]
         */
        public function getKitchen(): Collection
        {
            return $this->kitchen;
        }

        /**
         * @return Collection|Bedroom[]
         */
        public function getBedroom(): Collection
        {
            return $this->bedroom;
        }


        /**
         * @ORM\Id()
         * @ORM\GeneratedValue()
         * @ORM\Column(type="integer")
         */
        private $id;

        /**
         * @ORM\Column(type="string", length=255)
         */
        private $name;

        public function getId(): ?int
        {
            return $this->id;
        }

        public function getName(): ?string
        {
            return $this->name;
        }

        public function setName(string $name): self
        {
            $this->name = $name;

            return $this;
        }

        public function jsonSerialize()
        {
            return get_object_vars($this);
        }
    }

The House repository is empty (a.k.a: only containts the automatically generated code from Symfony):

    <?php

    namespace App\Repository;

    use App\Entity\House;
    use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
    use Symfony\Bridge\Doctrine\RegistryInterface;

    /**
     * @method House|null find($id, $lockMode = null, $lockVersion = null)
     * @method House|null findOneBy(array $criteria, array $orderBy = null)
     * @method House[]    findAll()
     * @method House[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
     */
    class HouseRepository extends ServiceEntityRepository
    {
        public function __construct(RegistryInterface $registry)
        {
            parent::__construct($registry, House::class);
        }

    }

The Bedroom entity is this:

    <?php

    namespace App\Entity;

    use Doctrine\ORM\Mapping as ORM;

    /**
     * @ORM\Entity(repositoryClass="App\Repository\BedroomRepository")
     */
    class Bedroom implements \JsonSerializable
    {

        /**
         * @ORM\ManyToOne(targetEntity="App\Entity\House", inversedBy="bedroom")
         */
        private $house;

        public function getHouse(): House
        {
            return $this->house;
        }

        public function setHouse(House $house): self
        {
            $this->house = $house;

            return $this;
        }


        /**
         * @ORM\Id()
         * @ORM\GeneratedValue()
         * @ORM\Column(type="integer")
         */
        private $id;

        /**
         * @ORM\Column(type="string", length=255)
         */
        private $name;

        public function getId(): ?int
        {
            return $this->id;
        }

        public function getName(): ?string
        {
            return $this->name;
        }

        public function setName(string $name): self
        {
            $this->name = $name;

            return $this;
        }

        public function jsonSerialize()
        {
            return get_object_vars($this);
        }
    }

and the Bedroom repository is also empty.

The Kitchen entity has the following code:

    <?php

    namespace App\Entity;

    use Doctrine\ORM\Mapping as ORM;

    /**
     * @ORM\Entity(repositoryClass="App\Repository\KitchenRepository")
     */
    class Kitchen implements \JsonSerializable
    {


        /**
         * @ORM\OneToMany(targetEntity="App\Entity\Cabinet", mappedBy="kitchen")
         */
        private $cabinet;

        /**
         * Kitchen constructor
         */
         public function __construct()
        {
            $this->cabinet= new ArrayCollection();
        }

        /**
         * @return Collection|Cabinet[]
         */
        public function getCabinet(): Collection
        {
            return $this->cabinet;
        }


        /**
         * @ORM\ManyToOne(targetEntity="App\Entity\House", inversedBy="kitchen")
         */
        private $house;

        public function getHouse(): House
        {
            return $this->house;
        }

        public function setHouse(House $house): self
        {
            $this->house = $house;

            return $this;
        }

        /**
         * @ORM\Id()
         * @ORM\GeneratedValue(strategy="UUID")
         * @ORM\Column(type="integer")
         */
        private $id;

        /**
         * @ORM\Column(type="string", length=255)
         */
        private $name;

        public function getId(): int
        {
            return $this->id;
        }

        public function getName(): string
        {
            return $this->name;
        }

        public function setName(string $name): self
        {
            $this->name = $name;

            return $this;
        }

        public function jsonSerialize()
        {
            return get_object_vars($this);
        }
    }

and the Kitchen repository is also empty. Finally, the cabinet consists of the following:

    <?php

    namespace App\Entity;

    use DateTime;
    use Doctrine\ORM\Mapping as ORM;

    /**
     * @ORM\Entity(repositoryClass="App\Repository\CabinetRepository")
     */
    class Cabinet
    {
        /**
         * @ORM\Id()
         * @ORM\GeneratedValue()
         * @ORM\Column(type="integer")
         */
        private $id;

        /**
         * @ORM\Column(type="string", length=255)
         */
        private $shopUrl;

        private $account_id;

        /**
         * @ORM\ManyToOne(targetEntity="Kitchen", inversedBy="cabinet")
         */
        private $kitchen;

        /**
         * Cabinet constructor.
         * @param string $shopUrl
         * @param Kitchen $kitchen
         * @param int $id
         */
        public function __construct(string $shopUrl, Kitchen $kitchen = null, int $id = null)
        {
            $this->shopUrl = $shopUrl;
            $this->kitchen = $kitchen;
            $this->id = $id;
        }

        public function setId(int $id): self
        {
            $this->id = $id;

            return $this;
        }

        public function getId(): int
        {
            return $this->id;
        }


        public function getShopUrl(): string
        {
            return $this->shopUrl;
        }

        public function getKitchen(): Kitchen
        {
            return $this->kitchen;
        }

        public function setKitchen(Kitchen $kitchen): self
        {
            $this->kitchen = $kitchen;
            $this->account_id = $kitchen->getId();
            return $this;
        }

        public function setAccount_id(int $account_id): self
        {
            $this->account_id = $account_id;

            return $this;
        }

        public function getAccount_id(): int
        {
            return $this->account_id;
        }

    }

In contrast to the other entities, the cabinet has some logic (this is where I actually need help). Since Bedroom and Kitchen are associated with a House, I would like to give a Bedroom, then look up all the kitchens associated with the same house as the Bedroom and then return all cabinets that these kitchens have. I know it may seem illogical but I discovered this too late to come up with another concept. My current code doesn't work because I'm not sure whether this is possible and because it's a bit too complex for me to grasp at this moment. But I have this as the content of the cabinet repo:


    <?php

    namespace App\Repository;

    use App\Entity\Bedroom;
    use App\Entity\House;
    use App\Entity\Cabinet;
    use App\Entity\Kitchen;
    use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
    use Symfony\Bridge\Doctrine\RegistryInterface;

    /**
     * @method Cabinet|null find($id, $lockMode = null, $lockVersion = null)
     * @method Cabinet|null findOneBy(array $criteria, array $orderBy = null)
     * @method Cabinet[]    findAll()
     * @method Cabinet[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
     */
    class CabinetRepository extends ServiceEntityRepository
    {
        public function __construct(RegistryInterface $registry)
        {
            parent::__construct($registry, Cabinet::class);
        }

        public function findByBedroom(Bedroom $bedroom) //use joins??
        {
            return $this->createQueryBuilder('cabinet')
                ->join('cabinet.bedroom', 'bedroom')
                ->join('cabinet.kitchen', 'kitchen')
                ->addSelect('cabinet')
                ->andWhere('cabinet.bedroom = :idBedroom')
                ->setParameter('idBedroom', $bedroom->getId())
                ->orderBy('time', 'ASC')
                ->getQuery()
                ->getResult();
        }
    }

I'm using PHPStorm and no error are showing anywhere but of course, the querybuilder doesn't return anything. How can I fix it? I couldn't find any questions that try to achieve what I'm trying to do.

I added data manually to the database, so there's data in there. And using Sequel Pro I can see the relations. The data (as far as I'm concerned) is fine. The queryBuilder is where the mistakes are.

A demonstrative example with some data:

This is the Bedroom data:

id     name              house_id  
325    bigBedroomOne     1666   
815    smallBedroomOne   555   
902    bigBedroomTwo     1666

This is the House data:

id     name          
1666   bigHouse      
555    smallHouse      

This is the Kitchen data:

id   name              house_id
1    bigKitchen        1666
2    smallKitchen      555
55   mediumKitchen     555

And finally, this is the cabinets data:

id  shopUrl    account_id
1   ur.l       55
88  co.m       2
33  ne.t       1

So in this example I would like to plug in the Bedroom id 815 which is associated with the house_id 555. Then from there all the Kitchen associated with that house_id, should be selected, so 2 and 55. Finally, the cabinets with id 1 and 88 should be returned.

Edit: When running bin/console doc:sch:val I get this back:

`Mapping

[OK] The mapping files are correct.

Database

[OK] The database schema is in sync with the mapping files.`

  • 1
    Before you ask, what does `bin/console doc:sch:val` say? – Mike Doe Jun 17 '19 at 20:39
  • Learn how to define associations properly. Don’t write any more code until you fix the mapping. And please. Don’t post this in comments. Always update your question. Everything you need is already in the Doctrine’s documentation. You just need to go through the docs thoroughly. Good night. – Mike Doe Jun 17 '19 at 21:09
  • @emix see my edit please! –  Jun 17 '19 at 21:43
  • Please update your entities as well in the question. What’s the $account_id in the model? Switch your thinking, you should not care about column names and joins, that’s ORM’s job, think about relations and business rules, that’s why models exist for. – Mike Doe Jun 18 '19 at 05:17

3 Answers3

1

In your debug bar in symfony you should probably be seeing some errors in doctrine. These won't show up in PHPStorm. You need to rename $kitchen and $bedroom to their plural forms $kitchens and $bedrooms (and change your getters/setters to match) since this is how you define things in the owning side of your doctrine relationships.

A simpler approach than your repository method would be to do what you want in your controller to let doctrine do your heavy lifting:

$cabinets = [];

$house = $bedroom->getHouse();
$kitchens = $house->getKitchens();
foreach ($kitchens as $kitchen) {
    $kitchenCabinets = $kitchen->getCabinets();
    $cabinets = array_merge($cabinets, $kitchenCabinets);
}
ehymel
  • 1,360
  • 2
  • 15
  • 24
  • Thanks for the suggestion. But I don't have a method called `getCabinets()` and I also thought that controllers shouldn't be doing such tasks. –  Jun 16 '19 at 15:06
  • Well, it's trivial to make one though. Just add the `$cabinets` property with a `OneToMany` relationship, just like you did in your `House` entity for `$kitchens` and `$bedrooms`, then make the function: `public function getCabinets() { return $this->cabinets; }`. – ehymel Jun 16 '19 at 15:15
  • One problem with your query is you try to join `cabinet.bedroom`. which does not exist. You would need to join `bedroom.house` as `house`, then `house.kitchens` as `kitchens`, then get all cabinets for each kitchen. I don't know if that's possible purely through queryBuilder. – ehymel Jun 16 '19 at 16:43
  • And don't forget to rename your variables/getters/setters in your `House` entity to plural forms where appropriate. It will help you keep everything straight. – ehymel Jun 16 '19 at 16:44
0

First I think because you joining with both entities at the same time in this

...
            ->join('cabinet.bedroom', 'bedroom')
            ->join('cabinet.kitchen', 'kitchen')
...

and because that will be with INNER JOIN, it will require that cabined is required both bedroom and kitchen cabinet.

For that there is few solutions to work through:

  • Proper one would be redesign you entities. I think it might not be hard to use Doctrine inheritance
  • you might change joins to left, so relation is not mandatory (will work, but in general its not good solution because of wrong design)
M. Kebza
  • 1,488
  • 1
  • 10
  • 14
  • Thanks for your response! Unfortunately, the documentation you sent is outdated. Would you please tell me how I could make it work as it is? How would the queryBuilder look like? –  Jun 16 '19 at 08:19
  • Its current version of doctrine, so unless you are using bleeding edge should be ok. You should change join calling of `join` to `leftJoin` – M. Kebza Jun 16 '19 at 12:38
  • But that isn't supposed to fix it, right? Because it still doesn't work. What other changes may be necessary? –  Jun 16 '19 at 13:01
0

There are several minor problems in your code, more on that later.

Here is \App\Repository\CabinetRepository::findByBedroom:

    public function findByBedroom(Bedroom $bedroom) //use joins??
    {
        return $this->createQueryBuilder('cabinet')
            ->join('cabinet.kitchen', 'kitchen')
            ->join('kitchen.house', 'house')
            ->join('house.bedroom', 'bedroom')
            ->addSelect('cabinet')
            ->andWhere('bedroom = :bedroom')
            ->setParameter('bedroom', $bedroom)
            ->getQuery()
            ->getResult();
    }

For bedroom entity with ID 815 the code above returns the following (formatted as symfony/var-dumper would do that):

array:2 [▼
  0 => Cabinet {#387 ▼
    -id: 88
    -shopUrl: "co.m"
    -account_id: null
    -kitchen: Kitchen {#354 ▼
      +__isInitialized__: false
      -cabinet: null
      -house: null
      -id: 2
      -name: null
       …2
    }
  }
  1 => Cabinet {#364 ▼
    -id: 1
    -shopUrl: "ur.l "
    -account_id: null
    -kitchen: Kitchen {#370 ▼
      +__isInitialized__: false
      -cabinet: null
      -house: null
      -id: 55
      -name: null
       …2
    }
  }
]

Note: house references are null because of lazy loading.

So, small problems in your code:

  1. Your query in CabinerRepository was doing wrong joins. For correct joins see code above.

  2. That query referring to unknown field time. I have removed that reference.

  3. And also was using bedroom ID instead of bedroom entity.

  4. Your Kitchen.php is incomplete, it refers Collection and ArrayCollection classes, but there are no corresponding use directives. Just add this after namespace before class:

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

Update: here is how to get repository reference:

public function myControllerAction(/* other parameters */, CabinetRepository $cabRepo)
{
    $cabinet = $cabRepo->find($id);
    // OR, if you don't want to add parameter for some reason:
    $cabRepo = $this->getDoctrine()->getRepository(Cabinet::class);
    $cabinet = $cabRepo->find($id);
}
alx
  • 2,314
  • 2
  • 18
  • 22
  • I don't understand "How did you pass an bedroom entity?" part. You can see my code, I just... pass it. I don't do `->getId()`. Why? Because I compare it with `bedroom`, not `bedroom.id`. That's how Doctrine works, it allows you to pass entities. Or IDs if you want, but in that case you need to compare it with `bedrrom.id`. I think either should work, but passing entity looks cleaner. – alx Jun 18 '19 at 12:47
  • That `new CabinetRepository` part looks wrong. Just add a parameter to you controller signature, and it will be autoinjected. Let me add example to my answer. – alx Jun 18 '19 at 12:53
  • I also try to do it like that `$temp = $this->getDoctrine()->getRepository('App\Entity\Cabinet')->findByBedroom($bedroom);` but then PHP storm that findByBedroom is not found –  Jun 18 '19 at 12:55
  • PhpStorms says that? What about running the code? The thing is, if PhpStorm is not configured (specifically, if Symfony plugin is not installed), it does not know what class is returned by `getRepository()` and cannot check if that method exists. So, this is a question of IDE configuration, and thus, not a real error. – alx Jun 18 '19 at 12:58
  • Here is how you can make sure Symfony plugin is there and working: https://plugins.jetbrains.com/plugin/7219-symfony-support – alx Jun 18 '19 at 13:00
  • Are you by any chance aware of a way to not print the house references at all? As in, only the cabinet references. –  Jun 19 '19 at 11:18
  • Sorry, can't understand the question. If you don't want to print something, don't print it? If you output entity as is, and the output includes something you don't want to see, you need to remove that before passing it to output. Details really depend on what exactly (entity? JSON? something else?) and how exactly (JsonResponse? some 3rd-party library? tons of options here) you output. – alx Jun 19 '19 at 11:44
  • Sorry if my question isn't clear. In your answer you use var-dumper to see print the cabinets and in addition to the cabinet information, House information are printed out. If I only want to return the cabinet info and not interested in the house stuff, how could I do that? Is it a matter of setting some settings out or is it something elsE? –  Jun 19 '19 at 13:07
  • You see house only because Doctrine automatically adds magical getters for all associations, and also because due to nature of the query (many JOINs) house was partially retrieved (particularly, ID field). The house itself is not retrieved (if you have Symfony debug bar enabled, you can see actual SQL queries that were executed). But you see some parts of it because you output Doctrine entity along with its magic. To avoid that, you need to convert entity, e.g. serialize it. You can do just `['id' => $bedroom->getId(), /* etc */]`. – alx Jun 19 '19 at 13:32
  • Or use advanced feature: https://stackoverflow.com/questions/48569537/symfony-4-serialize-entity-wihout-relations – alx Jun 19 '19 at 13:33
  • My very last question, where do you do that `['id' => $bedroom->getId(), /* etc */]` exactly? –  Jun 19 '19 at 13:43
  • Where you need your entity without "secret" or irrelevant fields? After you retrieved the entity, before you are ready to output it. – alx Jun 19 '19 at 13:54