Buy Access to Course
04.

Saving Entities

Share this awesome video!

|

Keep on Learning!

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:

19 lines | src/Controller/ArticleAdminController.php
// ... 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():

19 lines | src/Controller/ArticleAdminController.php
// ... 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:

19 lines | src/Controller/ArticleAdminController.php
// ... 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

19 lines | src/Controller/ArticleAdminController.php
// ... 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():

48 lines | src/Controller/ArticleAdminController.php
// ... 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:

48 lines | src/Controller/ArticleAdminController.php
// ... 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:

93 lines | src/Entity/Article.php
// ... 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:

48 lines | src/Controller/ArticleAdminController.php
// ... 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:

48 lines | src/Controller/ArticleAdminController.php
// ... 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:

48 lines | src/Controller/ArticleAdminController.php
// ... 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:

56 lines | src/Controller/ArticleAdminController.php
// ... 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():

56 lines | src/Controller/ArticleAdminController.php
// ... 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():

56 lines | src/Controller/ArticleAdminController.php
// ... 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.