Buy
Buy

Saving Entities

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Put on your publishing hat, because it's time to write some thoughtful space articles and insert some rows into our article table! And, good news! This is probably one of the easiest things to do in Doctrine.

Let's create a new controller called ArticleAdminController. We'll use this as a place to add new articles. Make it extend the normal AbstractController:

... lines 1 - 2
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
... lines 6 - 8
class ArticleAdminController extends AbstractController
{
... lines 11 - 17
}

And create a public function new():

... lines 1 - 8
class ArticleAdminController extends AbstractController
{
... lines 11 - 13
public function new()
{
... line 16
}
}

Above, add the @Route() - make sure to auto-complete the one from Symfony Components so that PhpStorm adds the use statement. For the URL, how about /admin/article/new:

... lines 1 - 6
use Symfony\Component\Routing\Annotation\Route;
class ArticleAdminController extends AbstractController
{
/**
* @Route("/admin/article/new")
*/
public function new()
{
... line 16
}
}

We're not actually going to build a real page with a form here right now. Instead, I just want to write some code that saves a dummy article to the database.

But first, to make sure I haven't screwed anything up, return a new Response: the one from HttpFoundation with a message:

space rocks... include comets, asteroids & meteoroids

... lines 1 - 5
use Symfony\Component\HttpFoundation\Response;
... lines 7 - 8
class ArticleAdminController extends AbstractController
{
/**
* @Route("/admin/article/new")
*/
public function new()
{
return new Response('space rocks... include comets, asteroids & meteoroids');
}
}

Now, we should be able to find the browser and head to /admin/article/new. Great!

Creating the Article Object

So, here's the big question: how do you save data to the database with Doctrine? The answer... is beautiful: just create an Article object with the data you need, then ask Doctrine to put it into the database.

Start with $article = new Article():

... lines 1 - 4
use App\Entity\Article;
... lines 6 - 9
class ArticleAdminController extends AbstractController
{
... lines 12 - 14
public function new()
{
$article = new Article();
... lines 18 - 45
}
}

For this article's data, go back to the "Why Asteroids Taste like Bacon" article: we'll use this as our dummy news story. Copy the article's title, then call $article->setTitle() and paste:

... lines 1 - 9
class ArticleAdminController extends AbstractController
{
... lines 12 - 14
public function new()
{
$article = new Article();
$article->setTitle('Why Asteroids Taste Like Bacon')
... lines 19 - 45
}
}

This is one of the setter methods that was automatically generated into our entity:

... lines 1 - 9
class Article
{
... lines 12 - 18
/**
* @ORM\Column(type="string", length=255)
*/
private $title;
... lines 23 - 48
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
... lines 55 - 91
}

Oh, and the generator also made all the setter methods return $this, which means you can chain your calls, like: ->setSlug(), then copy the last part of the URL, and paste here. Oh, but we need to make sure this is unique... so just add a little random number at the end:

... lines 1 - 9
class ArticleAdminController extends AbstractController
{
... lines 12 - 14
public function new()
{
$article = new Article();
$article->setTitle('Why Asteroids Taste Like Bacon')
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999))
... lines 20 - 45
}
}

Then, ->setContent(). And to get this, go back to ArticleController, copy all of that meaty markdown and paste here. Ah, make sure the content is completely not indented so the multi-line text works:

... lines 1 - 9
class ArticleAdminController extends AbstractController
{
... lines 12 - 14
public function new()
{
$article = new Article();
$article->setTitle('Why Asteroids Taste Like Bacon')
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999))
->setContent(<<<EOF
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow,
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow
**turkey** shank eu pork belly meatball non cupim.
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder,
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt
occaecat lorem meatball prosciutto quis strip steak.
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck
fugiat.
EOF
);
... lines 39 - 45
}
}

Much better! The last field is publishedAt. To have more interesting data, let's only publish some articles. So, if a random number between 1 to 10 is greater than 2, publish the article: $article->setPublishedAt() with new \DateTime() and sprintf('-%d days') with a bit more randomness: 1 to 100 days old:

... lines 1 - 9
class ArticleAdminController extends AbstractController
{
... lines 12 - 14
public function new()
{
$article = new Article();
$article->setTitle('Why Asteroids Taste Like Bacon')
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999))
->setContent(<<<EOF
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow,
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow
**turkey** shank eu pork belly meatball non cupim.
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder,
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt
occaecat lorem meatball prosciutto quis strip steak.
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck
fugiat.
EOF
);
// publish most articles
if (rand(1, 10) > 2) {
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100))));
}
return new Response('space rocks... include comets, asteroids & meteoroids');
}
}

Perfect! Now... stop. I want you to notice that all we've done is create an Article object and set data on it. This is normal, boring, PHP code: we're not using Doctrine at all yet. That's really cool.

Saving the Article

To save this, we just need to find Doctrine and say:

Hey Doctrine! Say hi to Jon Wage for us! Also, can you please save this article to the database. You're the best!

How do we do this? In the last Symfony tutorial, we talked about how the main thing that a bundle gives us is more services. DoctrineBundle gives us one, very important service that's used for both saving to and fetching from the database. It's called the DeathStar. No, no, it's the EntityManager. But, missed opportunity...

Find your terminal and run:

php bin/console debug:autowiring

Scroll to the the top. There it is! EntityManagerInterface: that's the type-hint we can use to fetch the service. Go back to the top of the new() method and add an argument: EntityManagerInterface $em:

... lines 1 - 5
use Doctrine\ORM\EntityManagerInterface;
... lines 7 - 10
class ArticleAdminController extends AbstractController
{
... lines 13 - 15
public function new(EntityManagerInterface $em)
{
... lines 18 - 53
}
}

Now that we have the all-important entity manager, saving is a two-step process... and it may look a bit weird initially. First, $em->persist($article), then $em->flush():

... lines 1 - 5
use Doctrine\ORM\EntityManagerInterface;
... lines 7 - 10
class ArticleAdminController extends AbstractController
{
... lines 13 - 15
public function new(EntityManagerInterface $em)
{
... lines 18 - 40
// publish most articles
if (rand(1, 10) > 2) {
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100))));
}
$em->persist($article);
$em->flush();
... lines 48 - 53
}
}

It's always these two lines. Persist simply says that you would like to save this article, but Doctrine does not make the INSERT query yet. That happens when you call $em->flush(). Why two separate steps? Well, it gives you a bit more flexibility: you could create ten Article objects, called persist() on each, then flush() just one time at the end. This helps Doctrine optimize saving those ten articles.

At the bottom, let's make our message a bit more helpful, though, I thought my message about space rocks was at least educational. Set the article id to some number and the slug to some string. Pass: $article->getId() and $article->getSlug():

... lines 1 - 5
use Doctrine\ORM\EntityManagerInterface;
... lines 7 - 10
class ArticleAdminController extends AbstractController
{
... lines 13 - 15
public function new(EntityManagerInterface $em)
{
... lines 18 - 45
$em->persist($article);
$em->flush();
return new Response(sprintf(
'Hiya! New Article id: #%d slug: %s',
$article->getId(),
$article->getSlug()
));
}
}

Oh, and this is important: we never set the id. But when we call flush(), Doctrine will insert the new row, get the new id, and put that onto the Article for us. By the time we print this message, the Article will have its new, fancy id.

Ok, are you ready? Let's try it: go back to /admin/article/new and... ha! Article id 1, then 2, 3, 4, 5, 6! Our news site is alive!

If you want to be more sure, you can check this in your favorite database tool like phpMyAdmin or whatever the cool kids are using these days. Or, you can use a helpful console command:

php bin/console doctrine:query:sql "SELECT * FROM article"

This is article with a lowercase "a", because, thanks to the default configuration, Doctrine creates snake case table and column names.

And... yes! There are the new, 6 results.

We have successfully put stuff into the database! Now it's time to run some queries to fetch it back out.

Leave a comment!

  • 2018-10-19 weaverryan

    Woohoo! SO happy this works for you :D. Now, keep going! ;)

  • 2018-10-18 Galen Senogles

    Oh my! You guys are great, and are already on it! I love these videos and this site. I hadn't noticed the "show all lines" button in the top right before, this is exactly what I needed, thank you!

  • 2018-10-18 weaverryan

    Hey Galen Senogles!

    Thanks for the feedback! I try to be aware of users not using PhpStorm... but even still... yea, we rely on it to do a lot of us (and, even if you're aware of what it's doing, you still can't see the use statements!). I *may* have one suggestion for you - if you haven't noticed it already: all of the code for each chapter is included below each video in code blocks. We only show the "important" parts by default (which should at least *usually* include the use statements, etc) but you can click a button to "expand" and see the whole file. You'd probably still need, for example, 2 tabs open - so you can switch from the video to the code blocks, but I hope it will help. It's one of our solutions for this exact problem.

    Let me know!

    Cheers!

  • 2018-10-17 Galen Senogles

    Hello, thanks a lot for these great videos. I just have a slight suggestion, and that is perhaps at the end of each video or at the bottom of the script, can all the includes at the top of the page, which includes the namespace declarations and the use declarations be included?

    For those of us who aren't using phpstorm, it is quite difficult to keep up with adding those into the file by hand if they aren't shown in the video. Taking the file itself from the finish directory of the included code doesn't always work because sometimes the file is completely different than how it is at the end of an episode.

    Most of my mistakes have came from not included something and being confused why I'm getting an error but eventually I figure it out.

    Thank you!

  • 2018-10-01 Diego Aguiar

    Hey Maxim Mandrik

    That's interesting, I believe the `QueryBuilder` for PostgreSQL may have some logic for converting your query string into a proper PostgreSQL query

    Cheers!

  • 2018-10-01 Victor Bocharsky

    Hey Maxim,

    Thanks for the detailed explanation! I actually didn't know that quotes are so important on Windows. Windows console sucks and have weird behavior sometimes :/

    Btw, I heard good feedbacks about this console emulator for Windows: https://github.com/cmderdev... . Just in case, maybe it might be useful for other users.

    Cheers!

  • 2018-09-28 Maxim Mandrik

    Interestingly, you can specify the table name with any case in the command:
    php bin/console doctrine: query: sql " SELECT * FROM ARTICLE"
    php bin/console doctrine: query: sql " SELECT * FROM article"
    php bin/console doctrine: query: sql 'SELECT * FROM "ARTIclE"'
    And everything works perfectly!

    I use PostgreSQL, that's how It behaves if I make queries not through Doctrine, but directly:
    https://stackoverflow.com/q...

    Probably Doctrine converts the query to lowercase?

  • 2018-09-28 Maxim Mandrik

    On Windows 10 in the standard cmd and in the terminal PhpStorm (which probably works on the basis of cmd):

    php bin/console doctrine:query:sql 'SELECT * FROM ARTICLE'
    Too many arguments, expected arguments "command" "sql".

    In PowerShell or MINGW64 works with any quotes.

  • 2018-08-21 Mamunur Rashid

    Thanks Victor Bocharsky ,
    Now it's working...

  • 2018-08-21 Victor Bocharsky

    Hey Rashid,

    Hm, I only can think that you trying to use this "Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Article" class somewhere in your code, see this reference: https://github.com/symfony/...

    I suppose you autocompleted a wrong namespace somewhere, try to find the place where you use that namespace. I think you need to replace it with "App\Entity\Article" one. Let me know if you still have this error.

    Cheers!

  • 2018-08-21 Mamunur Rashid

    Hello, I'm facing this problems,

    Attempted to load interface "NotExistingInterface" from namespace
    "Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation".
    Did you forget a "use" statement for another namespace?

    Symfony\Component\Debug\Exception\
    ClassNotFoundException

    in vendor\symfony\framework-bundle\Tests\Fixtures\Validation\Article.php (line 5)

  • 2018-07-11 Diego Aguiar

    Hey Scottie Gutman

    I believe the "TimeStampableEntity" will grab your server's timezone by default, so if you want to always make sure that the date is in UTC, you could do it via a listener, or by creating your own TimeStampableEntity which internally will make that conversion.

  • 2018-07-11 Scottie Gutman

    Storing all dates in the database in UTC was my conclusion too. So..... How can we modify the TimeStampableEntity Trait to do that? Or should we implement the conversion as described in the links?

  • 2018-07-11 weaverryan

    Hey Scottie Gutman!

    Ha! You just made my morning! I feel properly showered in praise - thanks ;).

    So..... great question - probably there aren't a lot of answers... because it's complicated... so nobody knows what to say :). First, do you need to track timezones / handle timezones? What I mean is, do you have situations where a user enters a time in their local timezone, and you need to store it in the database? Or, are your dates more internal - e.g. createdAt dates, etc?

    We only handle this in a few places in KnpU, but our solution is always to store dates in UTC. In the few places where our users need to enter a time (actually, this only happens in our admin), we make sure to convert the date from the user's timezone into a UTC DateTime before setting it on our entity. Ultimately, everything in the database is in UTC. IF we needed to re-render a date in a local timezone later (we don't have this requirement, but, if we did), we would just need to (A) know which timezone to convert to (which is why you see the timezone sometimes stored in the database - so you know how to convert it *back* later) and then (B) use that to convert the date when rendering it (but keep it in UTC on your entity). Heck, if you had an entity like Event that had a startDateTime field and a timezone field, you could add a new getStartDateInLocalTimezone() method that would create a new DateTime based on the startDateTime UTC and the timezone.

    Does that help? Or is there still a missing best-practice piece?

    Cheers!

  • 2018-07-10 Scottie Gutman

    Once again, you have made Symfony awesome for me, so let me shower some praise. ::shower::

    I don't know if this is the right place for this question, but it is regarding Doctrine/MySQL and TimeZones. I was thinking that it would be great for all the DateTime's in MySQL to be in UTC, so that east/west coast programming would not have to deal with EST/EDT. So I googled for an answer for some kind of best practice and did not find a consensus.

    I have relied on your Best Practice's and they have made my programming life so much easier (even when you make it more complicated :) ).

    So I found these to articles:
    https://www.doctrine-projec...
    https://blog.bestcoding.net...

    What are your thoughts on the subject and is this the best way to implement it. Or can it be "KNPUniversity'ed" up?

  • 2018-06-29 Dirk J. Faber

    I'll take your advice and will start using dependency injection and use the EntityManagerInterface.
    Thanks a bunch!

  • 2018-06-28 Diego Aguiar

    Yes, you are correct , this line $this->getDoctrine()->getManager(); returns you an instance of ObjectManager interface, which is the main abstraction for woking with ORM and ODM, but as I said, if you are going full ORM, then you don't have to worry about it and type-hint your dependencies with EntityManagerInterface.
    Oh, and if you are on Symfony3.3 or higher, then you should be using dependency injection all the time, in other words, do not use $this->getDoctrine() any more :)

    Cheers!

  • 2018-06-27 Dirk J. Faber

    Hi Diego,

    Thanks for taking the time to respond. I'm still somewhat confused between the ObjectManager (which seems to actually be the interface?) and the EntityManagerInterface. From an example on the Symofony website:


    // you can fetch the EntityManager via $this->getDoctrine()
    // or you can add an argument to your action: index(EntityManagerInterface $entityManager)
    $entityManager = $this->getDoctrine()->getManager();

    When I use this method, $entityManager is actually the ObjectManager according to my IDE. When I use: public function new(EntityManagerInterface $entityManager) then $entityManager is the EntityManagerInterface according to my IDE. Is that correct? And if so, does it not really matter in most cases if you're only going to use the ORM? Sorry I ask this again, but it is really confusing to me.

  • 2018-06-26 Diego Aguiar

    Thanks Dirk J. Faber!

  • 2018-06-26 Diego Aguiar

    Hey Dirk J. Faber

    Great question!
    The ObjectManager interface allows you to work with ORM and ODM, so if for some reason you are using both, or you are planning to switch one to another, then you should type hint your dependencies with "ObjectManager"
    The "EntityManagerInterface" contain some specific methods for working with a ORM, like "createQueryBuilder"
    and "EntityManager" is just the implementation of "EntityManagerInterface"

    No matter what, whenever you can, you should use interfaces for type-hinting your dependencies

    Cheers!

  • 2018-06-26 Dirk J. Faber

    No problem. Yes, i'm pretty sure it happened with autocompletion, because datetime_immutable is actually a type you could use.

  • 2018-06-26 Dirk J. Faber

    I've been using Symfony and Doctrine for a few months now, but there's always been a couple of things about Doctrine that made me wonder.
    In this example you have added the argument EntityManagerInterface $em to the new() method. Could you tell me why you choose the EntityManagerInterface over the EntityManager?

    I usually use $em = $this->getDoctrine()->getManager(); somewhere in the method where i need it. This works, but my IDE tells me that in this case, $em is actually the ObjectManager. Can you explain what the differences are between these three classes (EntityManagerInterface, EntityManager and ObjectManager) and when to use which?

  • 2018-06-26 Michael Thielen

    Perfect! Thanks.
    i wonder why it set it to datetime_immutable. Must have been some weird autocomplete when i created the entity.

  • 2018-06-26 Dirk J. Faber

    And if that is the case, simply change 'datetime_immutable' to 'datetime'.

  • 2018-06-26 Dirk J. Faber

    Could it be that you have the "type" of "publishedAt" in your Article class set to "datetime_immutable"? So in article controller you see something like:
    /**
    * @ORM\Column(type="datetime_immutable", nullable=true)
    */
    private $publishedAt;

    If so, that created the error, because you are setting this attribute to 'datetime_immutable' instead of 'datetime'.

  • 2018-06-26 Michael Thielen

    Got an error when i hit /admin/article/new at the end of the tutorial:
    Type error: Argument 1 passed to App\Entity\Article::setPublishedAt() must be an instance of DateTimeImmutable or null, instance of DateTime given, called in /var/www/phptut/src/Controller/ArticleAdminController.php on line 46
    Fixxed it by changing \DateTime to DateTimeImmutable in Line 46: $article->setPublishedAt(new \DateTimeImmutable(sprintf('-%d days', rand(1, 100))));

    can u explain what happened here? Cheers

  • 2018-06-11 Victor Bocharsky

    Hey Daniel,

    Hm, interesting, thanks for sharing it with others. But why not single quotes? Do you see some errors?

    Cheers!

  • 2018-06-09 Daniel

    Just a side note: Windows users should use double quotes on sql command: "SELECT * FROM article"