Buy

It's you again! Welcome back friend! In this tutorial, we're diving back into Doctrine: this time to master database relations. And to have fun of course - databases are super fun.

Like usual, you should code along with me or risk 7 years of bad luck. Sorry. To do that, download the code from the course page and use the start directory. I already have the code - so I'll startup our fancy, built-in web server:

./bin/console server:run

You may also need to run composer install and a few other tasks. Check the README in the download for those details.

When you're ready, pull up the genus list page at http://localhost:8000/genus. Nice!

Create the GenusNote Entity

Click to view a specific genus. See these notes down here? These are loaded via a ReactJS app that talks to our app like an API. But, the notes themselves are still hardcoded right in a controller. Bummer! Time to make them dynamic - time to create a second database table to store genus notes.

To do that, create a new GenusNote class in the Entity directory. Copy the ORM use statement from Genus that all entities need and paste it here:

80 lines src/AppBundle/Entity/GenusNote.php
... lines 1 - 2
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
... lines 6 - 10
class GenusNote
{
... lines 13 - 78
}

With that, open the "Code"->"Generate" menu - or Cmd + N on a Mac - and select "ORM Class":

80 lines src/AppBundle/Entity/GenusNote.php
... lines 1 - 2
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="genus_note")
*/
class GenusNote
{
... lines 13 - 78
}

Bam! This is now an entity!

Next: add the properties we need. Let's see... we need a username, an userAvatarFilename, notes and a createdAt property:

80 lines src/AppBundle/Entity/GenusNote.php
... lines 1 - 10
class GenusNote
{
... lines 13 - 17
private $id;
... lines 19 - 22
private $username;
... lines 24 - 27
private $userAvatarFilename;
... lines 29 - 32
private $note;
... lines 34 - 37
private $createdAt;
... lines 39 - 78
}

When we add a user table later - we'll replace username with a relationship to that table. But for now, keep it simple.

Open the "Code"->"Generate" menu again and select "ORM Annotation". Make sure each field type looks right. Hmm, we probably want to change $note to be a text type - that type can hold a lot more than the normal 255 characters:

80 lines src/AppBundle/Entity/GenusNote.php
... lines 1 - 10
class GenusNote
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string")
*/
private $username;
/**
* @ORM\Column(type="string")
*/
private $userAvatarFilename;
/**
* @ORM\Column(type="text")
*/
private $note;
/**
* @ORM\Column(type="datetime")
*/
private $createdAt;
... lines 39 - 78
}

Finally, go back to our best friend - the "Code"->"Generate" menu - and generate the getter and setters for every field - except for id. You don't usually want to set the id, but generate a getter for it:

85 lines src/AppBundle/Entity/GenusNote.php
... lines 1 - 10
class GenusNote
{
... lines 13 - 39
public function getId()
{
return $this->id;
}
public function getUsername()
{
return $this->username;
}
public function setUsername($username)
{
$this->username = $username;
}
public function getUserAvatarFilename()
{
return $this->userAvatarFilename;
}
public function setUserAvatarFilename($userAvatarFilename)
{
$this->userAvatarFilename = $userAvatarFilename;
}
public function getNote()
{
return $this->note;
}
public function setNote($note)
{
$this->note = $note;
}
public function getCreatedAt()
{
return $this->createdAt;
}
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
}
}

Generate Migrations

Entity done! Well, almost done - we still need to somehow relate each GenusNote to a Genus. We'll handle that in a second.

But first, don't forget to generate a migration for the new table:

./bin/console doctrine:migrations:diff

Open up that file to make sure it looks right - it lives in app/DoctrineMigrations. CREATE TABLE genus_note - it looks great! Head back to the console and run the migration:

./bin/console doctrine:migrations:migrate

Adding Fixtures

Man, that was easy. We'll want some good dummy notes too. Open up the fixtures.yml file and add a new section for AppBundle\Entity\GenusNote. Start just like before: genus.note_ and - let's create 100 notes - so use 1..100:

15 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... lines 1 - 8
AppBundle\Entity\GenusNote:
genus.note_{1..100}:
... lines 11 - 15

Next, fill in each property using the Faker functions: username: <username()> and then userAvatarFilename: Ok, eventually users might upload their own avatars, but for now, we have two hardcoded options: leanna.jpeg and ryan.jpeg. Let's select one of these randomly with a sweet syntax: 50%? leanna.jpeg : ryan.jpeg. That's Alice awesomeness:

15 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... lines 1 - 7
AppBundle\Entity\GenusNote:
genus.note_{1..100}:
username: <userName()>
userAvatarFilename: '50%? leanna.jpeg : ryan.jpeg'
... lines 13 - 15

The rest are easy: note: <paragraph()> and createdAt: <dateTimeBetween('-6 months', 'now')>:

15 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... lines 1 - 8
AppBundle\Entity\GenusNote:
genus.note_{1..100}:
username: <userName()>
userAvatarFilename: '50%? leanna.jpeg : ryan.jpeg'
note: <paragraph()>
createdAt: <dateTimeBetween('-6 months', 'now')>

Ok, run the fixtures!

./bin/console doctrine:fixtures:load

Double-check them with a query:

./bin/console doctrine:query:sql 'SELECT * FROM genus_note'

So awesome! Ok team, we have two entities: let's add a relationship!

Leave a comment!

  • 2017-08-14 Diego Aguiar

    Hey Luka Sikic

    I'm not sure if there is a best practice for it, but for teaching is better to show how you can create a new entity from scratch
    Anyways, feel free to create them however you please, as long as you don't introduce a bug :)

    Cheers!

  • 2017-08-13 Luka Sikic

    Hi Ryan,

    I wonder isn't it a better practice to use bin/console doctrine:generate:entity ?

  • 2017-05-03 weaverryan

    Hey Mike!

    Ah, no reason in particular :). I'm *super* accustomed (just from years of habit) to not having fluent setters. I also rarely (but sometimes) call multiple setters in a row. But I definitely don't see any issue with fluent setters!

    Cheers!

  • 2017-05-03 mike

    Why aren't you using fluent setters?

  • 2017-04-21 Victor Bocharsky

    Hey Julia,

    Great! I'm glad we're on the same wave. And no problem! Actually, it's good that you ask questions even quicker than we explain them in the course, well done!
    If you have any questions, do not hesitate to ask again ;)

    Cheers!

  • 2017-04-21 Julia Shishik

    Yes! All good! You did it in the next lesson!
    bin/console doctrine:database:drop --force
    bin/console doctrine:database:create
    It helped! Thank you
    I try to learn! Excuse me!

  • 2017-04-21 Victor Bocharsky

    Hey Julia,

    Well, the reason is simple: as you can see from the error message you already have the `genus` table in your DB, but your migration's trying to create this table. So most likely you're doing something wrong. I find it useful to read the docs for DoctrineMigrationsBundle to understand how it works, some base concepts of migrations and what this bundle provides to you out of the box: https://symfony.com/doc/cur...

    In shorts, the main question is *why* this `genus` table already exist on the time you run this migration at the first time? Probably you added this table with `bin/console doctrine:schema:create` or `bin/console doctrine:schema:update --force` command. But if you decided to control your DB with migrations, you have to forget about these create/update DB commands unless you have understanding of how migrations work at least.

    For now, to fix this error easy, you can try to remove your DB at all with `bin/console doctrine:database:drop --force`, then create only the DB with `bin/console doctrine:database:create` and after these actions you can start using migrations.

    Does it help to fix your error?

    Cheers!

  • 2017-04-21 Julia Shishik

    Good morning! Can you help me! when i do 'doctrine:migrations:migrate' I have error and create new table 'Migrating up to 20170421064206 from 0

    ++ migrating 20170418203924

    -> CREATE TABLE genus (id INT AUTO_INCREMENT NOT N....' 'SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'genus' already exists...'

    '[Doctrine\DBAL\Driver\PDOException]
    SQLSTATE[42S01]: Base table or view alrea
    dy exists: 1050 Table 'genus' already exi
    sts

    [PDOException]
    SQLSTATE[42S01]: Base table or view alrea
    dy exists: 1050 Table 'genus' already exi
    sts...'

  • 2017-03-03 Dan Costinel

    Hi Ryan.
    No problem!
    Indeed, I solved the problem regarding the aspect of the drop-down, using "selectize" js plugin.
    Thanks for support!

  • 2017-03-02 weaverryan

    Hey Dan!

    Sorry for the late reply! I wanted to reply 2 days ago... but AWS S3 was down, and so was Imgur! But now, I can see your screenshot :).

    First, happy the multiple worked! Because your Category is allowed to have an *array* of filters, this makes sense! I'm not sure why I missed that!

    Second, because the Category is allowed to have many filters... it doesn't really make sense to use a normal drop-down select anymore. That's why Symfony is rendering it in this way. If this were a normal drop-down, then you would only be able to choose one filter per Category. But, I also hate this "multi-select" - very not user friendly. If you want to use checkboxes instead, then add an 'expanded' => true option to the field. If you want to do something even *fancier* - like some sort of jQuery widget - then you'll probably want to keep the field as it is now, and then point that jQuery widget at the multi-select. Usually, these widgets work by hiding the true element (i.e. the multi-select) and then displaying the fancy widget. But, behind the scenes, when you choose things on the widget, it updates the multi-select.

    Let me know if that helps! And cheers!

  • 2017-02-24 Dan Costinel

    Hi Ryan.
    I think I finally found the problem. It seems that the problem is with that 'multiple' => false option. If it is set to true then the whole process works, and only when is set to false throws that error.

    Ok, done! But then I'm facing a new problem.
    My select is no more a default select, but this: http://imgur.com/a/j2BF2 .

    Any tip on how to make this one look like an usual select?

    Thanks!

  • 2017-02-23 weaverryan

    Yo Dan!

    1) Awesome :)

    2) Thanks for the code and screenshot! And you're right - this happens when the form is submitted. And I can see from your screenshot that the form component is trying to set a "filters" field on a Category object. So, even though you say this is happening when you submit the FilterType form, it is (somehow) your CategoryType that is causing the problem. Somehow, the CategoryType is being submitted, or is embedded in your form. And actually, I can see this in your exception screenshot - the categoryAction is being executed - not filterAction!

    Apart from that, even if your CategoryType *is* being submitted, it *should* in theory work: after all, you *do* have addFilter, removeFilter and getFilters() in Category. I'm honestly not sure what the issue is here - but I would try to add (or uncomment in your case) the by_reference option on your filters field in CategoryType.

    I hope this helps find the issue! Cheers!

  • 2017-02-23 Dan Costinel

    Thanks for replying Ryan, kind as always!

    1) Yeah, the system should start as an empty one. And I was kinda sure about the right answer, which is to choose one of the sides, and create some data for it, and then add data for the other entity. But I asked just to be sure!

    2). I create a new test project in symfony, and retyped all again, and still the problem persists, as I get the same error: "Error 500: Could not determine access type for property "filters".", even though I'm not declaring any "filters" form type in my FilterType.php file. Here's a gist of my entire test project: https://gist.github.com/dan... .

    Firstly, I thought this might be caused because I'm not adding any filter to a category, using the addFilter($category->getFilters()) Category class method. But then I realised the error is thrown previously to the submission (at least this is what I suppose), because it complains at the line which contains $form->handleRequest($request); (here's the print of the error: http://imgur.com/a/f4WsC )

    If you are able to see why this is happening, that would be great!

    Thank you!

  • 2017-02-22 weaverryan

    Yo Dan Costinel!

    Ok, let's do this! First, your setup looks cool - both relationships are setup correctly. In our collections tutorial (which goes into ManyToMany specifically), we do a bit more work in the adder method (you can see an example here: http://knpuniversity.com/sc... to make sure that we can't ever add duplicate links. This isn't your problem, just something to be aware of. Also, adding categories to your filter should work pretty easily, but adding filters to your category will mean that you are setting things from the *inverse* side of the relationship. So, you'll need to do a bit more work with that. But no worries, all stuff we talk about :) http://knpuniversity.com/sc...

    Now, let me answer your questions specifically

    > 1) how can I save, for the first time, information in any of the two tables?

    It depends on your requirements! Should the system start in an empty state? If so, then at some point, the user will need to create a Filter with no Categories or a Category with no Filters in order to start things off. But in many cases, your system might always start with a certain set of Categories (or Filters). In that case, I would add those in your fixtures.

    > 2) If I comment out the ->add('categories') from AppBundleForm/FilterType, and I try to add a new category and a filter for it, then I get this error: ...

    I think *this* is unrelated to uncommenting ->add('categories') - I think uncommenting that simply allows for your form to submit far enough to hit the error. The error seems to be saying that you have a "filters" form field on your FilterType! Because this is the error you get when a field exists on your form, but no getter/setter/adder/remover exists on the class to handle this. So, double-check the fields you have on your FilterType!

    I hope this gets you a step closer!

    Cheers!

  • 2017-02-22 weaverryan

    Ooh, good tip! In reality, it IS ok to use the existing project... unless I say otherwise (occasionally I make changes between courses, and I explicitly say to use the download code). But, I definitely do not make that crystal clear! We will make this more clear going forward!

  • 2017-02-21 Dan Costinel

    Hi KNP!

    I'm facing a problem and I can't find a solution, even though I'm following (at least I guess I do) the right "rules" for defining a ManyToMany relation (Category - Filters)
    I want to be able to add a list of filters for a specific category, and vice-versa, a list of categories for a specific Filter.
    For that I have to following code:


    # AppBundle/Entity/Category.php
    #...
    /**
    * @ORM\ManyToMany(targetEntity="Filter", inversedBy="categories")
    */
    private $filters;

    public function __construct() {
    $this->filters = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function addFilter(\AppBundle\Entity\Filter $filter) {
    $this->filters[] = $filter;
    }

    public function removeFilter(\AppBundle\Entity\Filter $filter) {
    $this->filters->removeElement($filter);
    }

    public function getFilters() {
    return $this->filters;
    }


    # AppBundle/Entity/Filter.php
    #...
    /**
    * @ORM\ManyToMany(targetEntity="Category", mappedBy="filters")
    */
    public $categories;

    public function __construct()
    {
    $this->categories = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function addCategory(\AppBundle\Entity\Category $category) {
    $category->addFilter($this);
    $this->categories[] = $category;
    }

    public function removeCategory(\AppBundle\Entity\Category $category) {
    $this->categories->removeElement($category);
    }


    public function getCategories() {
    return $this->categories;
    }

    For adding filters to a category, I have this form:


    # AppBundle/Entity/CategoryType
    #...
    ->add('filters', EntityType::class, [
    'class' => 'AppBundle:Filter',
    'placeholder' => ' ',
    'query_builder' => function(EntityRepository $er) {
    return $er->createQueryBuilder('f');
    },
    'choice_label' => function($filters) {
    return $filters->getValue();
    },
    'multiple' => false,
    'expanded' => false
    ])

    For adding categories to a filter, I have this form:


    ->add('categories', EntityType::class, [
    'class' => 'AppBundle:Category',
    'placeholder' => ' ',
    'query_builder' => function(EntityRepository $er) {
    return $er->createQueryBuilder('c');
    },
    'choice_label' => function($categories) {
    return $categories->getName();
    },
    'multiple' => false,
    'expanded' => false
    ])

    When I render any of the two forms, all things goes ok, no problems here.
    Questions:

    1. how can I save, for the first time, information in any of the two tables? Because, if I want to add a filter for a category, then the dropdown for this first category is empty - as there are no filters yet. And vice-versa, if I want to add a category for a filter, the dropdown containing categories is empty - as there are no categories yet.

    2. if I comment out the ->add('categories') from AppBundleForm/FilterType, and I try to add a new category and a filter for it, then I get this error:


    Error 500: Could not determine access type for property "filters".

    Any ideas on how to solve this two issues?

    Thank you!

  • 2017-02-21 maxii123

    In the introductory text it might be an idea to say whether our existing project (from the end of fixture data) is sufficient to begin this course.

  • 2016-11-14 Nobuyuki Fujioka

    Hi, Victor
    Thank you for your reply. My first try seemed to have worked. But, will see.
    I will try your solution as well.

    I have many other questions. so, will ask you later.

    Cheers,
    Noby

  • 2016-11-14 Victor Bocharsky

    Hey Nobuyuki,

    I think you can. Have you tried it? Btw, what about just map $id field to the different column name in database:


    /**
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    * @ORM\Column(name="connector_id", type="integer")
    */
    private $id;


    This should work as well.

    Cheers!

  • 2016-11-13 Nobuyuki Fujioka

    Hi,

    When I create an entity class, can I have different names for id like below?
    (@ORM\Id and private $connectorId)
    /**
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    * @ORM\Column(type="integer")
    */
    private $connectorId;

    Cheers,
    Noby

  • 2016-11-11 Victor Bocharsky

    You're welcome!

    Cheers!

  • 2016-11-11 Nobuyuki Fujioka

    Hi, Victor

    Thank you very much. I understand it better now thanks to your explanation.

    Kind regards,
    Noby

  • 2016-11-11 Victor Bocharsky

    Hey Nobuyuki,

    My PhpStorm creates setters and getters with annotation too, so probably Ryan has different settings, or just an older version of PhpStorm, or it just was a bug in PhpStorm :)

    This annotation called PHP DocBlock comments. You can read more about it here: https://phpdoc.org/docs/lat... . Basically, this annotations helps IDEs like PhpStorm to do autocompletion. Usually, it's unnecessary for IDE because it can resolve returned type base on @var property annotation. The types of method arguments can be resolved base on typehint, but it don't work for scalar type, that's why @param annotation for methods with scalar values would be good here. But nothing critical won't happen without these annotations for getters/setters, you just don't have an autocompletion in some places of your code.

    Actually, here's my recent merged PR to the Symfony Demo project about annotation: https://github.com/symfony/... . I think you might be interested in it - there's a small discussion about annotation best practice.

    Cheers!

  • 2016-11-11 Nobuyuki Fujioka

    Just a quick question, in your video, when you create getter and setter for a parameter in GenusNote class, phpStorm is creating getter and setter functions without annotations. But, with my PHPstorm, it is creating annotation for each one.
    Below is an example:
    ```
    /**
    * @return mixed
    */
    public function getUsername()
    {
    return $this->username;
    }

    /**
    * @param mixed $username
    */
    public function setUsername($username)
    {
    $this->username = $username;
    }
    ```
    What are these
    /**
    * @return mixed
    */
    and
    /**
    * @param mixed $username
    */
    ?

    Do we need them?

    Cheers,
    Noby

  • 2016-09-28 Victor Bocharsky

    haha, yes, it's :D

  • 2016-09-28 Maksym Minenko

    Ryan would say: "But that's so boooring" :D

    Ok, ok, it's quite suffice for me, thank you! ;)

  • 2016-09-28 Victor Bocharsky

    Yes, agree, but I think that's the simplest solution. BTW, I just use username: user<current()>.

  • 2016-09-28 Maksym Minenko

    Do you mean like this, for example?
    username: <username()>_<current()>

    Well, not the most elegant solution, but... it works. :) I guess it's quite ok for development purposes.

  • 2016-09-28 Victor Bocharsky

    Yo Maksym,

    The <current()> function return a current index of each iteration. Combine it with username to make it 100% unique.

    Cheers!

  • 2016-09-28 Maksym Minenko

    Out of 100 usernames three are exactly the same. Is there an easy way to make them unique?

  • 2016-08-10 Victor Bocharsky

    Hey Henry,

    In Symfony project: just change "database_driver" to "pdo_sqlsrv" instead of "pdo_mysql" in your "app/config/parameters.yml". For custom project based on Doctrine - find a place where configuration parameters are setting according to the Doctrine configuration.

    P.S. And also don't forget to tweak other DB credentials as host, port, username and password.

    Cheers!

  • 2016-08-09 Henry

    Hi how can I connect to a MSSQL database with doctrine please?

  • 2016-07-05 Victor Bocharsky

    Hey Ilya,

    Great! I think the problem was that you used `createAt` instead of `createdAt`, right? It should be `createdAt`. Your explanation could help other devs with similar problem.

    Cheers!

  • 2016-07-05 Ilya

    It's ok evrything is workin

  • 2016-07-05 Ilya

    Hi,guys
    after load bin/console doctrine:fixtures:load
    unexpectedvalueexception could not determine how to assign createAt to a AppBundle\Entity\GenusNote object

  • 2016-06-14 weaverryan

    I like this stuff - good tips!

    And no reason about server:start versus server:run. I feel like new people may have less trouble with server:run, as they may accidentally keep server:start running in the background, then come back later, try to start it again, and get an error.

    Cheers!

  • 2016-06-14 JLChafardet

    weaverryan

    I just noticed, I have a couple of little things that you could integrate into your files to help the viewers in the long run.

    create a symlink for bin/console
    ln -s bin/console console
    add an alias for the console command
    (be it on .aliases or your .bash_rc file)
    alias console="php console"

    type "console"

    See the results here http://goo.gl/n1P4CU

    oh my terminal? well thats powerline-shell -> https://github.com/milkbiki... <- at work (linux here, works in the mac too for those interested)

    also I notice you use php bin/console server:run and not server:start ? any particular reason for it?

  • 2016-05-11 Ленур

    thanks, problem solved :)

  • 2016-05-11 weaverryan

    Ah, make sure you have the PHP Annotations plugin installed for PhpStorm. In the first course, I did *not* mention this - we're making an edit to the video to add this missing item!

  • 2016-05-08 Ленур

    I do you *not* see the option at all.

  • 2016-05-07 weaverryan

    Hi again!

    Hmm, is it inactive, or do you *not* see the option at all? Make sure your cursor is *inside* your entity class when you use the Code->Generate menu.

    Let me know what you find out :)

  • 2016-05-07 Ленур

    Hello Ryan.

    How to enable ORM Annotation on code generate menu? In my menu this option inactive.

    Thanks.