Symfony: Doctrine & the Database

With already two courses checked off, you are primed and ready to put Symfony to work! And that's what this course is all about: making things more interesting by talking to the database with Doctrine.

Doctrine has a reputation for being tough to learn, and that's unfortunate, because it's an amazing tool. In this tutorial, we'll get you rolling with Doctrine: learn how to use it, how to get hard work done easily and how to avoid the common pitfalls that can make Doctrine hard:

  • Creating entity classes
  • Managing migrations
  • Saving new data
  • Querying for data
  • Custom repositories
  • Custom queries and the query builder
  • Fixtures (Dummy data) using Faker

... and (like always) a lot more goodies are mixed in.


Your Guides

Ryan Weaver Leanna Pelham

Questions? Conversation?

  • 2016-10-15 Benson Twumasi

    Thanks Man

  • 2016-10-15 weaverryan

    Hey Beson!

    Awesome! To your questions:

    1) It's possible (I have never done it, but others certainly have) to reverse engineer your existing database in order to create entities that match it. Exactly how successful this will be depends on how clean or messy your current database is. Here are the details: http://symfony.com/doc/current...

    2) If your empBirthday field is a Doctrine datetime or date type (e.g. @ORM\Column(type="date"), which it should be, then what you should set on this property is a DateTime *object* - not just a date string. The reason you're getting this error is that Doctrine is expecting a DateTime object, so it's trying to call format() on your string when inserting it into the database. So, your code should look something like this:


    $employee->setEmpBirthday(new \DateTime('1984-11-12'));

    Even though this will store as a string in the database, you'll always deal with DateTime objects with Doctrine, which is actually pretty nice. Later, if you wanted to print the birthday, it would be something like:


    echo $employee->getEmpBirthday()->format('Y-m-d');

    Cheers!

  • 2016-10-15 Benson Twumasi

    Ryan,
    it worked. You re Genus. lol. Thanks.

    However l have a new challenges.

    1.how do i use exiting database in my projects instead of recreating using doctrine.
    2. i have update my database with birthday. l get this date format error
    "Call to a member function format() on a non-object" when inserting to the tables using this

    $employee->setEmpBirthday('1984-11-12');

  • 2016-10-15 weaverryan

    Hi Benson!

    Welcome to Symfony! I can definitely help you with this - it's just one *small* thing :). In your route, you have `/employee/{employeeName}`. This means that you are allowed to have a `$employeeName` argument to your controller. But, you have `$employeeId`. Symfony is basically saying:

    > Hey! I see `$employeeId` as an argument to showAction(), but I don't see a {employeeId} in your route, so I don't know what value to pass here!

    The fix is to make these match - probably in your case by changing the route to `/employee/{employeeId}`. Also, when you do this, if you have any links to this route (the employee_show route), you'll need to update those as well to now pass employeeId instead of employeeName (http://knpuniversity.com/scree....

    Let me know if this helps!

    Cheers!

  • 2016-10-15 Benson Twumasi

    <tr>
    <td>
    {{ employee.empId }}
    </td>
    <td>{{ employee.empFirstname }}</td>
    <td>{{ employee.empLastname }}</td>

  • 2016-10-15 Benson Twumasi

    hi Ryan

    Am trying my hands on my first symfony project and having this difficulties.

    Controller "AppBundle\Controller\EmployeeController::showAction()"
    requires that you provide a value for the "$employeeId" argument. Either
    the argument is nullable and no null value has been provided, no
    default value has been provided or because there is a non optional
    argument after this one.

    this the code

    viewEmployeeList.html.twig',


    <tr>
    <td>
    {{ employee.empId }}
    </td>
    <td>{{ employee.empFirstname }}</td>

    EmployeeController.php


    /**
    * @Route("/employee/{employeeName}", name="employee_show")
    */
    public function showAction($employeeId)
    {

    $employee = $this->getDoctrine()->getManager();
    $employeeId =$employee->getRepository('AppBundle:Employee')
    ->findOneBy(['employeeId' =>$employeeId]);

    if (!$employee) {
    throw $this->createNotFoundException('employee not found');
    }
    $this->get('logger')
    ->info('Showing Employee: '.$employeeId);

    return $this->render('employee/viewEmployeeDetails.html.twig', array(
    'name' =>$employeeId,
    ));
    }
    }
  • 2016-08-31 Victor Bocharsky

    Hey Marcelo,

    Could you debug it by running bin/console doctrine:schema:validate command? And let me know its result. It shows whether you have any invalid mapping.

    BTW, you probably use `user` column name instead of `user_id` in some of your indexes or uniqueConstraints. Please, recheck that all your entities (not only Course entity) have `user_id` column name instead of `user` inside @ORM\Table() annotation. Actually, you have to check all the entities which have relation to the User entity.

    Cheers!

  • 2016-08-30 Marcelo

    Hi there!
    I'm having the same problem:
    My class:


    /**
    * Course
    *
    * @ORM\Table(
    * name="course",
    * uniqueConstraints={
    * @ORM\UniqueConstraint(name="unique_course", columns={"name", "institution_id", "user_id"})
    * },
    * indexes={
    * @ORM\Index(name="IDX_169E6FB9A76ED395", columns={"user_id"})
    * }
    * )
    * @ORM\Entity(repositoryClass="MainBundle\Entity\CourseRepository")
    * @UniqueEntity(
    * fields={"name", "institution", "user"},
    * errorPath="name",
    * message="unique.course.name"
    * )
    * @ORM\HasLifecycleCallbacks
    */
    class Course extends AbstractEntity
    /**
    * @var \MainBundle\Entity\User
    *
    * @ORM\Column(name="user_id")
    * @ORM\ManyToOne(targetEntity="MainBundle\Entity\User", inversedBy="series")
    * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
    */
    private $user;


    Then when I run:


    console doctrine:migrations:diff
    [Doctrine\DBAL\Schema\SchemaException]
    There is no column with name 'user' on table 'course'.
  • 2016-08-15 Henri Tompodung

    Thank you Ryan!

  • 2016-08-11 weaverryan

    Hi Henri!

    It depends :). I'm not really sure how the database looks - as it's really custom to your application. The first step would be to setup a Menu entity with the proper relationships. Once you have that, it should be pretty easy to figure out how you would do all of this.

    BUT, there is always another option. If you have a really custom query, there's nothing wrong with writing and using a raw SQL query, instead of doing things through Doctrine. In fact, in some cases, this might be the best idea. We talk about how to do that here: http://knpuniversity.com/scree...

    Cheers!

  • 2016-08-09 Henri Tompodung

    How to make recursive function in symfony 3 with doctrine?
    I have script PHP native like below:

    mysql_connect("localhost","root","blablabla");
    mysql_select_db("demo");

    function html_menu(&$strmenu="", $parent=0) {
    $query = "SELECT * FROM menu
    WHERE code_parent='$parent'
    ORDER BY code_menu";
    $sql = mysql_query($query);

    if (mysql_num_rows($sql) > 0) {
    $strmenu .= '<ul>'.PHP_EOL;
    }

    // show children
    while ($row = mysql_fetch_array($sql)) {
    $strmenu .= "<li>".PHP_EOL;
    $strmenu .= sprintf(" % s ", $row['link'], $row['nm_menu'], $row['nm_menu']).PHP_EOL;

    html_menu($strmenu, $row['code_menu']);
    $strmenu .= "</li>".PHP_EOL;
    }

    if (mysql_num_rows($sql) > 0) {
    $strmenu .= '</ul>'.PHP_EOL;
    }

    }

    $strmenu = "";
    html_menu($strmenu, 0);
    echo $strmenu;

  • 2016-07-20 JLChafardet

    weaverryan yeah! suddenly i became swamped in work, a company in the netherlands hired some hours (i work as a freelance contractor some hours a week aside from my business, you know, extra coins wont hurt ever, as long as they dont take up sleep time nor weekends [I myself used to work even weekends for the past 18 years, lost a lot of time on that, so now im properly taking care of not just my mental health but also my body's health and my spiritual peace haha]) so got 2 projects going on my 8h/day mon-fri work routine, 3 extra hours a day contracted in the NL for this laravel project, havent had much time to browse or comment! but I take to heart everything I learnt here! I'm still much inexperienced, but im growing with each line of code i write, getting to know symfony better bit by bit.

    regarding laravel, well its indeed nice, it has a lot of out of the box features that are really cool, yet it feels way too much microframeworkish, thats just overgrown, it doesnt feel by any means a fully featured framework in and on itself, but as stated a "overgrown" microframework. I may be wrong tho.

  • 2016-07-20 weaverryan

    Hey! No worries :) Laravel is not my preference, but it's nice too - you can probably bring back some nice tools/lessons from it back to Symfony.

    I look forward to having you back! - I noticed you hadn't commented in awhile ;)

  • 2016-07-20 JLChafardet

    weaverryan my subscription expired, but ill renew it next month, this month im tied down horribly with a laravel project that i gota take care of. (:S didnt want to write in laravel, but the customer's mind is dead set on it, wanted to try to get them to use symfony)

  • 2016-07-20 weaverryan

    Awesome :) - glad you got it figured out. It's configurable, but out-of-the box with Doctrine in Symfony, property names are "snaked-cased" like this, which I kinda like (but it can surprise at first!)

    Cheers JLChafardet!

  • 2016-07-19 JLChafardet

    ok my bad, found the issue, was on the indexes, as im using isRead variable, but the table name is in fact "is_read"

  • 2016-07-19 JLChafardet

    hey guys, I'm having an issue with the migrations here.


    /**
    * @ORM\Column(type="boolean")
    */
    private $isRead;

    when I run console doctrine:migrations:diff i get the following error

    jl@josefo-d: /var/www/vhosts/project-s3 on master [!?]
    $ console doctrine:migrations:diff


    [Doctrine\DBAL\Schema\SchemaException]
    There is no column with name 'isRead' on table 'contact'.

    what could be the issue here?

  • 2016-06-28 JLChafardet

    Consider this tho: this works flawlessly in propel! (just for argument sake, if it works in propel, out of the box, no extra bundle required, i think it would be great for doctrine to have it too).
    back on topic:
    yeah i think ill get rid of loggable/versionable behavior. as the translatable one is more important for the customer, offering "versions" was an idea of mine and i actually never really offered it, i just "wanted" to give it as an extra

    i was pointed at https://github.com/DATA-DOG/Da... seems its a more robust audit tool, which could serve, ill consider it for later implementation to see if it works in my favor

  • 2016-06-28 weaverryan

    Those two behaviors specifically will be very difficult to get working together. And it kind of makes sense - versioning is already hard in a relational database (you need to duplicate each row before it's changed). Add onto that the fact that each row now has many translations, and you quickly (and correctly) get a big, ugly matrix. I use some of the more magic behaviors sparingly - learning from Doctrine version 1 that the magic has costs. For example, on our apps, we don't use Loggable/versionable or soft deletable.

    So, my best advice is to consider doing *less* - these behaviors try to make a relational database do things that it doesn't naturally do well. If you layer on too many, I don't think you'll be happy.

    Cheers!

  • 2016-06-28 JLChafardet

    seems is not possible to use them together. i hate this, i really dont want to implement my own behaviors.

  • 2016-06-27 JLChafardet

    ok, getting on my way to properly have Versioned and Translatable working together! what a freaking long day.

    the issue seems to be related to the fact that to add a new language you have to actually "edit" and when you "edit" it generates a new version.

    its pissing me off badly.

  • 2016-06-27 JLChafardet

    translatable and versionable (loggable) dont seem to be friendly. adding a new translation is interpreted by loggable as a new version of the row.

  • 2016-06-27 JLChafardet

    i seem to be advancing on the translations, ill keep you posted lol

  • 2016-06-27 weaverryan

    Yep, I am aware of this - it's tricky because if you *do* include the unfolded code blocks, then some of them are HUGE (in some tutorials, we show a cached file that is a few thousand lines long) and you also lose perspective of the 5 lines that we want you to focus on. But, if they're folded, then sometimes you can't see enough :). I don't think there's a perfect way - except to check out the code on the site ;)

    Cheers!

  • 2016-06-27 weaverryan

    Nice work JLChafardet

    I think what happens in a lot of cases with Doctrine is that:

    A) it requires more typing to set something up and
    B) The documentation often isn't clear (so that it's difficult to know what to type!)

    Anyways - nice job, and thanks for sharing your thoughts - I definitely have this on the "idea" list :)

    Cheers!

  • 2016-06-27 JLChafardet

    I think it would do great to have a full blown tutorial/track coverage.

    I've been playing with the Versioned and Translatable behaviors, and at least in 5 hours of work, i CANNOT get them to work properly.

    heres the basic main reason why now that I see how Dogtrine works why some people prefer to use Propel.

    I'm going to keep at it, as i just cant accept defeat, but this is just ridiculously complex for something that should be ridiculously simple.

    getting "Versioned" fields doesnt seem to work on the fly as it should, what i mean is this:
    Entity:


    namespace AppBundle\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Gedmo\Mapping\Annotation as Gedmo;
    use Gedmo\Translatable\Translatable;

    /**
    * @ORM\Entity
    * @Gedmo\Loggable()
    * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
    * @ORM\Table(name="blog", indexes={@ORM\Index(name="title_idx", columns={"title"}),@ORM\Index(name="slug_idx", columns={"slug"})})
    */
    class Blog implements Translatable
    {
    /**
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    * @ORM\Column(type="integer", unique=true)
    */
    private $id;

    /**
    * @Gedmo\Translatable()
    * @Gedmo\Versioned()
    * @ORM\Column(type="string")
    */
    private $title;

    /**
    * @Gedmo\Timestampable(on="create")
    * @ORM\Column(type="datetime")
    */
    private $createdAt;

    /**
    * @Gedmo\Timestampable(on="update")
    * @ORM\Column(type="datetime", nullable=true)
    */
    private $updatedAt;

    /**
    * @ORM\Column(type="datetime", nullable=true)
    */
    private $deletedAt;

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

    public function getTitle()
    {
    return $this->title;
    }

    public function setTitle($title)
    {
    $this->title = $title;
    return $this;
    }

    }
    ?>

    Controller:


    namespace AppBundle\Controller;

    use AppBundle\Entity\Blog;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;

    class BlogController extends Controller
    {
    /**
    * @Route("/test")
    */
    public function newAction()
    {
    //we insert
    $blog = new Blog();
    $blog->setTitle('Titulo 1 ');
    $blog->setDescription('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
    $blog->setMainImage('image');
    $blog->setContent("

    Praesent eleifend facilisis sapien a lobortis. Aliquam semper feugiat lectus. Cras non ligula nec odio gravida maximus sed eu sem. Quisque in ornare sem odio

    ");
    $blog->setTranslatableLocale('es_ES');
    $em = $this->getDoctrine()->getManager();
    $em->persist($blog);
    $em->flush();

    //we retrieve (comment the above code and run the code below this line)
    $em = $this->getDoctrine()->getManager();
    $blogEm = $this->getDoctrine()
    ->getRepository('AppBundle:Blog');
    $articles = $blogEm->findAll();

    dump($articles);
    //IT DOESNT get any of the Versioned fields.

    the answer from the dump is:

    BlogController.php on line 36:
    array:1 [▼
    0 => Blog {#565 ▼
    -id: 1
    -title: ""
    -slug: "article-one-1"
    -mainImage: "image"
    -description: ""
    -content: ""
    -isVisible: false
    -locale: null
    -createdAt: DateTime {#561 ▼
    +"date": "2016-06-27 15:45:36.000000"
    +"timezone_type": 3
    +"timezone": "America/Caracas"
    }
    -updatedAt: DateTime {#562 ▼
    +"date": "2016-06-27 15:45:36.000000"
    +"timezone_type": 3
    +"timezone": "America/Caracas"
    }
    -deletedAt: null
    }
    ]
  • 2016-06-27 JLChafardet

    BTW! weaverryan are you aware that when you download the transcript for the courses, the code is folded when the pdf is created?

    you should look into that, so the pdf is generated with the code entirely expanded.

  • 2016-06-27 JLChafardet

    Heya weaverryan !

    Tried alone first, pure doctrine extensions, installation was a bit rough, but went ok! tho couldnt get translatable to work, then went with StofDE and got it working, believe it or not, was touger to get StofDE working tho.

    just find really irritating it seems translatable uses a single table to translate everything, will have to see that a little bit ahead! i will port some apps from silex to s3, but on my free time, for now gota keep up working on what gota be done hehe

  • 2016-06-26 weaverryan

    Yo JLChafardet!

    You beat me to it - but I was going to recommend gedmo - so very happy you found it. It's a great library, though sometimes its documentation isn't AS great as it could be :). Are you using it with StofDoctrineExtensionBundle? It doesn't do much, but if helps alleviate some of the setup work (but it's docs aren't great). I'm not sure about Translatable - that's one I haven't used yet.

    But yea, the extensions are super fun - it might be good for a mini-tutorial :)

    Cheers!

  • 2016-06-26 JLChafardet

    ok I've been playing with the gedmo/doctrine-extensions, seems i've gotten most of it to work, except translatable, or at least im not seeing it create any tables for the translations, loggable, softdeletable, timestampable, sluggable tho seems to work just fine.

    gota keep playing with it it seems.

    dogtrine's learning curve is really step! oh well, who doesnt love a good challenge eh!

  • 2016-06-26 JLChafardet

    yo yo weaverryan

    hows all going? I have a question, lol highly important to me atm (maybe its covered in a different tutorial yet i havent had much time lately to browse around, if so would you please point me in the right direction?).

    I'm interested in knowing about doctrine behaviors, for example, I use a lot in propel timestampable, softdelete and well of course, i18n
    I'm not sure how to even begin to work that out on doctrine the annotations. i checked you have a knp bundle called doctrine-behaviors but they seem to be overly complicated to use, is there nothing of the sort native out of doctrine as they are entirelly and totally native in propel?

    I'm attempting to port a project i wrote in silex1+propel1 into S3+dogtrine, to practice and well if possible actually really migrating it, and those behaviors are a requirement.

  • 2016-06-01 David Thorne

    Great introduction Ryan! Thanks. Keep up the good work.

  • 2016-04-26 weaverryan

    Hi Andrés!

    Hmm, that usually means that you are trying to persist and entity (e.g. Genus) - but Doctrine does not think it's an entity. For example, if you mistakenly put `Genus` in a directory called `Model` instead of `Entity`, then Doctrine wouldn't know to read its annotation metadata, and it would look like a normal class, not an entity. This would cause this error. It's a very uncommon error, exception in edge-cases when you're working with third-party libraries that give you entities. In those cases, sometimes you have to add configuration so Doctrine is aware of the extra entity classes :).

    I hope that helps!

  • 2016-04-24 Andrés Vr

    Hello Ryan

    when I create an entity I have this error:
    The class 'xxx' was not found in the chain configured namespaces

    Do you have any idea to resolve them??

  • 2016-04-21 Shairyar Baig

    Hi,

    All i had to do was move '$currentProfileImage = $college->getLogo();' at the beginning of the action and it worked :)

    Thanks for your comments and looking into it anyways.

    Baig

  • 2016-04-21 weaverryan

    Hey Shairyar!

    Hmm, I'm not sure. But! To find out, here's what I would do. Suppose that the persisted property on College for the image is called imagePath. First, I'd see if anything in College modifies the imagePath property other than setImagePath(). If anything else *does* modify imagePath, then temporarily comment that out and edit the College - see if the image is lost. Also, throw a new \Exception() in setImagePath() to see if something is calling it on College edit.

    My guess is that you might be setting $this->imagePath = null; inside setLogo() or something similar, but I'm not sure :).

    Cheers!

  • 2016-04-21 Shairyar Baig

    Hi Ryan,

    I am stuck in a situation and hoping that your expertise will push me in right direction.

    It is a really simple task, in admin panel i need to give admin an option to add image and add the description, this is working fine but as soon the admin edit's the description and saves it the image name becomes null in database. so it works the first time and then later on, on edit the image name goes missing.

    This is the action, in this i am using `currentProfileImage ` and saving the existing image in case user does not provide the new one while editing so i dont lose the only that is already there, but this does not work.



    public function editAction(Request $request, College $college)

    {

    $em = $this->getDoctrine()->getManager();
    $editForm = $this->createForm('AppBundle\Form\CollegeType', $college);
    $editForm->handleRequest($request);
    $currentProfileImage = $college->getLogo();

    if ($editForm->isSubmitted() && $editForm->isValid()) {

    if ($editForm->get('logo')->getData() == null) {

    // if an image has not been provided
    $college->setLogo($college->getLogo());

    $em->persist($college);

    } else {

    $profileImageDir = $this->container->getParameter('kernel.root_dir') . '/../web/uploads';

    $file = $college->getLogo();

    // Generate a unique name for the file before saving it
    $fileName = md5(uniqid() . $file) . '.' . $file->guessExtension();

    // Move the file to the directory where profile images are stored
    $file->move($profileImageDir, $fileName);
    $college->setLogo($fileName);

    $em->persist($college);
    }

    $em->flush();

    //display successful message
    $this->get('session')->getFlashBag()->add(
    'success',
    $this->get('translator')->trans('general_success_message')
    );

    return $this->render('AppBundle:admin/college:edit.html.twig', array(
    'college' => $college,
    'edit_form' => $editForm->createView(),
    ));

    }

    return $this->render('AppBundle:admin/college:edit.html.twig', array(
    'college' => $college,
    'edit_form' => $editForm->createView(),
    ));

    }

    What am i doing wrong here?

  • 2016-01-27 weaverryan

    Hey there!

    Welcome! Yes, it's not exactly a beginner problem, but maybe I can help:

    1) The only thing that will live in your repository are queries. From flat PHP, think of it this way: any time that you *used* to literally write a query - e.g. $sql = 'SELECT * FROM ...', followed by $conn->execute($sql) or something similar - put that logic as a new method in a repository. That's the only purpose of a repository: to try to prevent query logic from being all around your project. If you're selecting from multiple tables, just put the new method in the repository that you feel most appropriately fits.

    2) Depending on your user flow, your form might be quite complicated for this. If the user is creating an event, then adding a drop-down of all of the themes in the system is simple - just use the EntityType. But, if you need to do some crazy AJAX logic once the user selects the theme (e.g. in order to load a list of products in that theme, or something) - that can get more complicated. My advice on the form system is always this: use it when it works for you. But if you get in a real complex situation, it's ok to revert back and use some normal HTML forms and process the input manually in your controller (a bit more like flat PHP)

    3) If you need to do some validation after a form is submitted to see if the required products are available, then you can do that in your controller (and just render the template with an error message if there is a problem). If you want to get a bit more advanced, you can create a custom validation constraint. I talk about some of the possibilities on this post: https://knpuniversity.com/scre...

    Overall, don't worry too much. Symfony gives you a lot of tools, but optional tools. If you feel overwhelmed, it's ok to choose to use less of Symfony's tools, while you get going :).

    Good luck!

  • 2016-01-27 Bettinz

    Hello, this is a beginner question :D I've an entity called product, one called event, and one called theme. To make an event, you must select a theme; based on theme, the event will require some products and I need to check if that products are available. I need to write custom queries (like select quantity from product_in_theme and verify if is available in product) but..where I write that code? In repository? If yes, the full code, will be in repository? (For example: check the theme passed with form, select the product from the form, etc). I know it's not the beginner situation, and maybe years writing plain php is giving me the wrong idea, so please, help me :)

  • 2016-01-20 weaverryan

    Hey Sudhir! I'm still aiming for the next few weeks :)

  • 2016-01-20 Sudhir Gupta

    hi , when it will released??
    I are awaiting like hungry

  • 2016-01-06 weaverryan

    Hey Igor!

    This is the 2nd episode in the Symfony 3 series after knpuniversity.com/screencast/s..., which we're just finishing now. My hope is to get this episode this month. In the mean time, Doctrine hasn't changed that much, so here are some links that might be useful:

    * https://knpuniversity.com/scre... (and the chapter after)
    * https://knpuniversity.com/scre...
    * and most of https://knpuniversity.com/scre...

    I hope that helps!

  • 2016-01-05 Igor Lula

    Hello there, when this screencast will be released?