Buy

Creating an Entity Class

Yo guys! Time to level-up our project in a big way. I mean, big. This series is all about an incredible library called Dog-trine. Wait, that's not right - it's Doctrine. But anyways, Doctrine is kinda like a dog: a dog fetches a bone, Doctrine fetches data from the database. But Doctrine does not pee in the house. That's part of what makes it awesome.

But back up: is Doctrine part of Symfony? Nope. Symfony doesn't care how or if you talk to a database at all. You could use a direct PDO connection, use Doctrine, or do something else entirely. As usual, you're in control.

If you want to code along - which you should - then download the code from the screencast page and move into the directory called start. I already have the start code downloaded, so I'll go straight to opening up a new terminal and starting the built-in sever with:

./bin/console server:run

Tip

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

Perfect!

Doctrine is an ORM

Doctrine is an ORM: object relational mapper. In short, that means that every table - like genus - will have a corresponding PHP class that we will create. When you query the genus table, Doctrine will give you a Genus object. Every property in the class maps to a column in the table. Keep this simple idea in mind as we go along: this mapping between a table and a PHP class is Doctrine's main goal.

Oh, and before we hop in, I want you to remember something very important: all the tools in Symfony are optional, including Doctrine. If Doctrine - or any tool - does more harm than good while solving a problem, skip it and do something simpler. Tools are meant to serve you, not the other way around.

Your First Entity Class

Our sweet app displays information about different ocean-living genuses... but so far, all that info is hardcoded. That's so sad.

Instead, let's create a genus table in the database and load all of this dynamically from there. How do you create a database table with Doctrine? You don't! Your job is to create a class, then Doctrine will create the table based on that class. It's pretty sweet. Oh, and the whole setup is going to take about 2 minutes and 25 lines of code. Watch.

Create an Entity directory in AppBundle and then create a normal class inside called Genus:

8 lines src/AppBundle/Entity/Genus.php
... lines 1 - 2
namespace AppBundle\Entity;
class Genus
{
}

You're going to hear this word - entity - a lot with Doctrine. Entity - it sounds like an alien parasite. Fortunately, it's less scary than that: an entity is just a class that Doctrine will map to a database table.

Configuration with... Annotations!

To do that - Doctrine needs to know two things: what the table should be called and what columns it needs. To help it out, we're going to use... drumroll... annotations! Remember, whenever you use an annotation, you need a use statement for it. This will look weird, but add a use for a Column class and let it auto-complete from Doctrine\ORM\Mapping. Remove the Column part and add as ORM:

15 lines src/AppBundle/Entity/Genus.php
... lines 1 - 2
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
... lines 6 - 15

Tip

You can also configure Doctrine with YAML, XML or PHP, instead of annotations. Check Add Mapping Information to see how to configure it.

Every entity class will have that same use statement. Next, put your cursor inside the class and open up the "Code"->"Generate" menu - cmd+N on a Mac. Ooh, one of the options is ORM Class. Click that... and boom! It adds two annotations - @ORM\Entity and @ORM\Table above the class:

15 lines src/AppBundle/Entity/Genus.php
... lines 1 - 6
/**
* @ORM\Entity
* @ORM\Table(name="genus")
*/
class Genus
{
}

Doctrine now knows this class should map to a table called genus.

Configuring the Columns

But that table won't have any columns yet. Lame. Add two properties to get us rolling: id and name. To tell Doctrine that these should map to columns, open up the "Code"->"Generate" menu again - or cmd+N. This time, select ORM Annotation and highlight both properties. And, boom again!

25 lines src/AppBundle/Entity/Genus.php
... lines 1 - 6
/**
* @ORM\Entity
* @ORM\Table(name="genus")
*/
class Genus
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string")
*/
private $name;
}

Now we have annotations above each property. The id columns is special - it will almost always look exactly like this: it basically says that id is the primary key.

After that, you'll have whatever other columns you need. Hey, look at the type option that's set to string. That's a Doctrine "type", and it will map to a varchar in MySQL. There are other Doctrine types for strings, floats and text - we'll talk about those soon!

And with just 25 lines of code, we're done! In a second, we'll ask Doctrine to create the genus table for us and we'll be ready to start saving stuff. Well, let's get to it!

Leave a comment!

  • 2017-07-19 Diego Aguiar

    Hey Juan Nicolás

    An entity in Doctrine represents a record in a table for your DB, so that's why you see a lot of DB metadata, such as relationships, field types, etc. Most of the time they have pure data, but you can put more logic on it (Business logic), the only constraint is, an Entity can't access to any services you have, so you can't do "User->login();"
    For that case you may want to have a UserManager service

    Cheers!

  • 2017-07-19 Juan Nicolás

    I'm getting crazy.
    I saw more videos from this course and I hope that here is the correct place to ask.

    I don't know if a Entity it's the same as a Model into MVC. For example, before this course I was doing something like "Model > User > function login()" (whitout Symfony, of course), but now, I don't sure if inside a "User entity" I can put functions like these or only Doctrine should use that Entity.

    Thanks you so much, I really apreciate all your help, teacher.

  • 2017-07-13 Josk

    Yes, It helped. Thank you you're the best!

  • 2017-07-12 Victor Bocharsky

    Hey Josk,

    Most likely it's due to the cache problems. In which environment do you see this error: prod, dev or test? Try to clear the cache. I even recommend you to do it manually with $ rm -rf var/cache/* console command. Let us know if it doesn't help.

    Cheers!

  • 2017-07-12 Josk

    Hi, sorry to bother many times, your support tutorials and support for students is great by the way.
    I've installed composer, but I've got this error:
    [Symfony\Component\Debug\Exception\ContextErrorException]
    Warning: Class __PHP_Incomplete_Class has no unserializer
    It worked at the start of this tutorial, but after all the changes when I tried to restart the server It gets me the error above

  • 2017-06-16 Diego Aguiar

    Hey Loes!

    I believe you just forgot to run $ composer install
    Give it a try, and if is not the case, let me know

    Ah, also there is a Readme file, it might help you too

    Cheers!

  • 2017-06-16 Loes Visser

    Hey, I downloaded the source code and copied the start folder in my project folder. But now I get an error;

    (1/1) ClassNotFoundException
    Attempted to load class "KnpMarkdownBundle" from namespace "Knp\Bundle\MarkdownBundle".
    Did you forget a "use" statement for another namespace?
    in AppKernel.php (line 19)

    line 19 in AppKernel.php is;

    new Knp\Bundle\MarkdownBundle\KnpMarkdownBundle(),

    How can I fix this?

  • 2017-05-14 weaverryan

    And thanks for the really nice words! Definitely get that Symfony Plugin - it's AMAZING!!! :D

  • 2017-05-12 Victor Bocharsky

    Hey MiChAeLoKGB,

    Thanks for your kind words!

    Sure! It's not a secret plugin and we try to mention it in our screencast every time :) It's a Symfony Plugin, which you can install and then enable in PhpStorm preferences. Please, check out this free screencast to get more details how to install and use it: https://knpuniversity.com/s...

    Cheers!

  • 2017-05-11 MiChAeLoKGB

    Similar to Islam, I have never paid for a single online course... Yours is the first and IMO its worth it.

    I have a question tho. It seems that you have a PhpStorm plugin that shows you the value of Symfony YAML constant when you click on it. Would you mind sharing its name? As I am not able to find it.

  • 2017-05-03 weaverryan

    You made my day! Cheers! And yea, Symfony gives you a lot of cool stuff. Or, said differently.... there is a lot to know to use all of Symfony :).

    Keep up the good work yourself!

  • 2017-05-03 Islam Elshobokshy

    Symfony is not hard, it just got SOO many things that one need to remember. So the difference between your tutorials and others', is that you try to explain it simply while still covering all the important parts and aspects. Keep up the great work !

  • 2017-05-03 KnpUniversity

    Ah that's so nice! I'm delighted that you're finding the tutorials helpful :D

  • 2017-05-02 Islam Elshobokshy

    I usually never buy courses on the internet, but it's the first time I actually find paying $25 worth it. Best dollars spent online in my life haha

  • 2017-04-25 Victor Bocharsky

    Hey Dennis,

    Thanks for sharing it!

    Cheers!

  • 2017-04-25 Dennis

    And I do not have the ORM class there :(

  • 2017-04-25 Dennis Eikelboom

    Maybe useful to know, for a Windows computer to open the Code generate inside the class (at 3:56) use the shortcode Alt + Insert.

  • 2017-04-10 Gremet Laurent

    Okay Victor, thanks for your help, this will be usefull for me in the future.

  • 2017-04-10 Victor Bocharsky

    Hi Gremet,

    Oh, I see... Well, I think the article How to Generate Entities from an Existing Database could help for you. I think most cases should work with it, but anyways, if you're going to use Doctrine ORM for that DB, I'm afraid you have to do some tweaks in DB schema before your schema will be valid. Actually, it depends on *that* DB, difficult to say, so as I said you have to try it: fill in credentials for "db_erp" and execute `bin/console doctrine:schema:validate` to see if it will work.

    Cheers!

  • 2017-04-10 Gremet Laurent

    Hi Victor, Thanks.
    In fact i think about an application that could work with several databases.
    And in this case those databases would not have been built with doctrine.
    For instance a symphony application working with two databases : one database called "db_specific" built with doctrine, and another database called "db_erp" already existing and being also used by others applications.

  • 2017-04-10 Victor Bocharsky

    Hey Gremet,

    If this table was generated with Doctrine - most likely so. You can easily check it by yourselves: when DB credentials is pointed, execute the next command:

    $ bin/console doctrine:schema:validate

    It will show you whether your DB schema is OK.

    Cheers!

  • 2017-04-08 Gremet Laurent

    Hi everybody,

    I have a question about Doctrine : if, i have to connect to an existing database, with tables and foreign keys, will
    Doctrine still be okay to do that ?

    Thanks

  • 2017-01-30 Dan Costinel

    Thanks a lot for the example, I'll try it immediately, and I'll let you know when I'm done.

    LE

    You're a life savior! Works as expected.
    So, to resume, for the ones beginners, like me:
    - in Product entity, I have $photo field (+ other fields you might want), with getter and setter
    - in ProductType, I have this field:


    ->add('photoFile', FileType::class, [
    'data_class' => null,
    'multiple' => true,
    'label' => null,
    'mapped' => false,
    ])

    In the edit action method in the controller, I needed to create another form, containing the same fields, but this time without the 'multiple' option, because the user can edit one product at a time, so no multi-upload functionality involved.


    /**
    * @Route("/edit/{id}", name="edit_route")
    */
    public function editAction(Request $request, Product $product)
    {
    $form = $this->createFormBuilder($product)
    ->setMethod('POST')
    ->setAction($this->generateUrl('edit_route',['id' => $product->getId()]))
    //add extra fields here, if any
    ->add('photoFile', FileType::class, [
    'data_class' => null,
    'label' => null,
    'mapped' => false,
    ])
    ->getForm()
    ;
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
    $photo = $form['photoFile']->getData();
    if(null != $photo){ // if the user did selected another image in order to edit the exiting one...
    $img = $this->get('app.image_upload')->upload($form['photoFile']->getData()); //...upload it
    $product->setPhoto($img); //set the name of the photo for saving it into the db
    //set extra fields here
    $this->getDoctrine()->getManager()->flush(); // save the image name into db

    return $this->redirectToRoute('homepage');
    } else { // if the user didn't wanted to edit the image, update only the extra fields, if he updated any of them
    $this->getDoctrine()->getManager()->flush();

    return $this->redirectToRoute('homepage');
    }
    }

    return $this->render('default/edit.html.twig', ['form'=>$form->createView(), 'product' => $product]);
    }

    In the default/edit.html.twig template, the form for


    {{ form_start(form) }}

    {% if product.photo %}
    <img src="{{ asset('upload/' ~ product.photo) }}" width="400" height="200">
    {% endif %}
    {{ form_row(form.photoFile) }}


    <button type="submit">Go</button>
    {{ form_end(form) }}

    Now, if a user edits or not any of the fields inside the edit form, when he submits it, it won't get null as the value for the photo field inside the db anymore. Or, if the user wants to upload a different image, then this new image will be saved instead of the old one.

  • 2017-01-30 weaverryan

    Hey Dan!

    Hmm, that solution is great... but seems like overkill to me. So, assuming that your edit screen is a nice, traditional product edit, where you're editing just *one* product, then there is a 2 step process:

    1) In your form, you need to call your field something *other* than "photo". The problem is that the "photo" property on your entity is a string filename. But when you use a File field in your form, when you submit, this will be an UploadedFile object. So, temporarily (before you do the processing), your string field is actually an object... which is weird (then later, you move the uploaded file and re-set this field to the filename string). But, on edit, this causes a real problem because if you do *not* choose to upload a new file, then when you submit, suddenly your "photo" field is overwritten with "null". And that's not what we want! So, rename your field to "photoFile" and give it a special option 'mapped' => false This makes it "ok" that you don't have an photoFile property on your Product entity. But you can still access the value like you did before: $form['photoFile']->getData() (except that this time, this should be a single object, not an array, since you can only edit one photo at a time on your edit screen).

    2) With *just* the above change, if the user doesn't re-upload a file, it won't replace the value stored in the database. Well, to make sure this happens, you'll surround your upload logic with an if statement:


    // see if the user actually uploaded something! If not, do nothing
    if ($form['photoFile']->getData()) {
    // do the file processing stuff
    // and set the photo property on your Product entity
    }

    3) In your template, everything is simple: just render your photoFile upload field. If you want to get fancy and render the current image, you can totally do that. Just make sure to pass your product as a variable into the template


    {% if product.photo %}
    {# this line will change based on where you are storing your uploads #}
    <img src="{{ asset('uploads/'~product.photo) }}"/>
    {% endif %}

    {{ form_row(form.photoFile) }}

    Cheers!

  • 2017-01-30 Dan Costinel

    Yes, the user will edit just one product at a time. The products table has many fields, but for the simplicity of the question (to be understandable), I've posted just that field I was interested at.

    I've found a solution on SO: http://stackoverflow.com/a/.... Do you consider it still valid(and the way of solving the problem) for Symfony 3.2.1?

    Thanks!

  • 2017-01-30 weaverryan

    Hey Dan!

    > So should I instantiate a new product object inside the foreach loop, and then set $product->setPhoto($img), then persist + flush?

    Yep, exactly :). On the edit page, will the user be editing just *one* product at a time, or will they be editing many products at once (similar to how the new page allows you to create multiple products)? Does this product table have other fields? I think I was confused because it seems odd to have a "products" table that only has an image field :).

    Cheers!

  • 2017-01-30 Dan Costinel

    Yes, you've understood correctly. If one selects 5 images, then I need to upload each image to the specified directory (functionality already done & working), *and* I need to insert 5 paths into the db, to the respective images.
    So should I instantiate a new product object inside the foreach loop, and then set $product->setPhoto($img), then persist + flush?

    And where you guide me to read more about editing such file filed?

    Thanks for the effort!

  • 2017-01-30 weaverryan

    Yo Dan!

    So, upload can be tricky :). But, I'm sure we can get this right!

    1) I clearly see that you have multiple "photo" fields in your form (you're looping over these). But, I'm a little confused, because - according to your code - if I upload 5 photos, then you want 5 records to be inserted into the products table. Is this correct? I was expecting that maybe you wanted 1 Product, but then 5 inserts into some related "product_photos" table. But, let's assume that you DO want 5 products (which is totally valid - I am probably just misunderstanding). In that case, you need to create 5 "new Product" objects. Currently, there is only one Product object, so Doctrine inserts the first time through the loop (which it hits $em->flush()</code),>

  • 2017-01-30 Dan Costinel

    Hi
    Two questions regarding doctrine:

    1) when I want to save to the db the images from a multi-select form field, I can't save them using doctrine's way. Example:


    /**
    * Creates a new product entity.
    *
    * @Route("/new", name="product_new")
    * @Method({"GET", "POST"})
    */
    public function newAction(Request $request)
    {
    $product = new Product();
    $form = $this->createForm('AppBundle\Form\ProductType', $product);
    $form->handleRequest($request);

    $cat = $request->query->get('category');
    $category = $this->getDoctrine()->getRepository('AppBundle:Category')->findOneby(['name'=>$cat]);
    if (!$category) {
    throw $this->createNotFoundException('The Category '.$category.' does not exist');
    }

    if ($form->isSubmitted() && $form->isValid()) {
    $images = $form->get('photo')->getData();
    foreach ($images as $img){
    //this is a
    $name = $this->get('app.image_uploader')->upload($img);

    // $product->setPhoto($name);
    // $product->setActive('yes');
    // $product->setOrder(99);
    // $product->setCategory($category);
    // $em = $this->getDoctrine()->getManager();
    // $em->persist($product);
    // $em->flush();

    $em = $this->getDoctrine()->getManager();
    $RAW_QUERY = 'INSERT INTO products SET photo=?, active="yes", order=99, category_id=?';
    $stmt = $em->getConnection()->prepare($RAW_QUERY);
    $stmt->execute([$name, $categoria->getId()]);
    }

    $this->addFlash('success', 'The products has been successfully created');

    return $this->redirectToRoute('category_show', array('id' => $category->getId()));
    }

    return $this->render('product/new.html.twig', array(
    'product' => $product,
    'category' => $category,
    'form' => $form->createView(),
    ));
    }

    The doctrine's way is the commented code, and it just inserts the last element of the loop. Strange thing is that all the images are uploaded correctly into the upload directory, only the insertion into db doesn't work. I figured out that, somehow, when my images are uploaded, they don't have the right owner (I'm using Ubuntu). It should be dan dan, but instead it's www-data www-data. And because the upload is getting run before the db saving, this might cause the problem?

    2) After saving the images to the db, I'm having a page to edit the record, and beside other fields, I need to edit the image too. The problem is that I can't figure out a way to prepopulate the file field with the saved image for that product.
    So if my product is:


    id name photo
    1 candy candy.png

    I want my file field inside the edit form to be pre-populated with the candy.png value. Because, if the user doesn't want to edit the image, but only the name, if he submits the form, the new value for the photo field inside the db, will be null.

    How can I achieve this?

    Thanks!

  • 2017-01-05 weaverryan

    Hi Hermen!

    Good question.. and it basically... it depends. I often don't worry about the length... other than making sure to use "text" instead of "string" if I know it'll be longer than 255. But if you have a "string" field and decide to set its length specifically (e.g. to 100), you're doing this purely so that your database is a little bit smaller. Is this is a good thing? Of course (unless you set it to 100 and then later try to save strings longer than 100 - I have totally done this in the past)! Will it make a big difference? Hmm, probably not - unless you really need to optimize things. So, for the most part, I treat setting the length explicitly as "premature optimization" - I have better things to do... and we can always shorten this column later. Of course, a database-administrator would argue with me... but I will probably launch my project faster ;).

    P.S. LAME on the comments - we use Disqus... which is a GIANT in the commenting world... so it's crazy that they validate incorrectly against valid domains. Hopefully they'll fix it - but sorry about that!

    Cheers!

  • 2017-01-05 Hermen

    So, when I set up a text field, I don't think about how long the information is that goes in that field? I just leave the standard length set to 255? Or does a responsible developer still think about the maximum size of a field?

    By the way, your comments do not allow for the new e-mail addresses (tld solutions for example)...

  • 2016-10-28 Victor Bocharsky

    Hey Terry,

    Yes, you're right - it's the best practice to use lowercase for modifiers and other reserved keywords.

    Cheers!

  • 2016-10-26 Terry Caliendo

    Nevermind. It does autocomplete... just not if you start with capital "P". Doh!

  • 2016-10-26 Terry Caliendo

    Why doesn't PHP Storm autocomplete "Private" when I start typing it? Seems like that should be a common thing to help autocomplete.

  • 2016-09-12 Victor Bocharsky

    Hey Ugur,

    Ah, that's because you didn't install Composer dependencies. You need to run $ composer install after download a course. Check the README.md in the "start/" directory of downloaded code.

    Cheers!

  • 2016-09-11 ugur ertas

    I copied the project from start and I'm getting this erro when I run php bin/console server:run

    PHP Warning: require(/home/uertas/Downloads/symfony-doctrine/start/app/../vendor/autoload.php): failed to open stream: No such file or directory in /home/uertas/Downloads/symfony-doctrine/start/app/autoload.php on line 11

  • 2016-07-26 weaverryan

    Hi Ammar!

    Also install the "PHP Annotations" plugin - I neglected to mention this in the first course (we've added a note, but it's still easy to miss). This will likely fix your issue :).

    Cheers!

  • 2016-07-25 Ammar

    I don't get any options to generate annotations. Any idea why is this happening? Do i need to add some kind of configurations to Phpstorm?

    Here is the screenshot
    http://imgur.com/a/EZ5sa

  • 2016-06-14 JLChafardet

    Entirelly true, +1 thereader to your post!
    I'm usually the kind that gets bored at this kind of videos, normally never really watching for long, the casters (screencasters) tend to be dead boring, but here at kpnu is a whole different story! I'm as engaged as I can be! just started yesterday and I'm already finding myself in the situation wake up->go to work->work fast if possible->""agr wana get home to continue knpu tracks""->4pm->lock workstation->drive home->login knpu

  • 2016-05-23 kokers

    I was about to say the same. Diggingt the narration of your courses gals and guys.

  • 2016-03-17 thereader

    Dogtrine .. You rule sir. lol

    I really like the way you teach. It really motivates me to listen to you. Keep up the good work.

    P.S : I will surely save some bucks to buy a subscription on this site ..

    Regards,
    Kev
    From Mauritius