Lets Generate a PDF!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet's transform this Twig template into a PDF.
Back in AuthorWeeklyReportSendCommand
, right before we create the Email
, this is where we'll generate the PDF, so we can attach it. To do that, our command needs two new services: Environment $twig
- yes, it looks weird, but the type-hint to get Twig directly is called Environment
- and Pdf $pdf
. That second service comes from SnappyBundle.
// ... lines 1 - 6 | |
use Knp\Snappy\Pdf; | |
// ... lines 8 - 16 | |
use Twig\Environment; | |
// ... line 18 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
// ... lines 21 - 28 | |
public function __construct(UserRepository $userRepository, ArticleRepository $articleRepository, MailerInterface $mailer, Environment $twig, Pdf $pdf) | |
{ | |
// ... lines 31 - 37 | |
} | |
// ... lines 39 - 84 | |
} |
As a reminder, if you don't know what type-hint to use, you can always spin over to your terminal and run:
php bin/console debug:autowiring pdf
There it is!
Ok, step 1 is to use Twig to render the template and get the HTML: $html = $this->twig->render()
. Oh... PhpStorm doesn't like that... because I forgot to add the properties! I'll put my cursor on the new arguments, hit Alt+Enter, and select "Initialize Fields" to create those 2 properties and set them.
// ... lines 1 - 6 | |
use Knp\Snappy\Pdf; | |
// ... lines 8 - 16 | |
use Twig\Environment; | |
// ... line 18 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
// ... lines 21 - 25 | |
private $twig; | |
private $pdf; | |
public function __construct(UserRepository $userRepository, ArticleRepository $articleRepository, MailerInterface $mailer, Environment $twig, Pdf $pdf) | |
{ | |
// ... lines 31 - 35 | |
$this->twig = $twig; | |
$this->pdf = $pdf; | |
} | |
// ... lines 39 - 84 | |
} |
Now, back to work: $this->twig->render()
and pass this the template name - email/author-weekly-report-pdf.html.twig
- and an array of the variables it needs... which I think is just articles
. Pass 'articles' => $articles
.
// ... lines 1 - 18 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
// ... lines 21 - 46 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
// ... lines 49 - 53 | |
foreach ($authors as $author) { | |
// ... lines 55 - 59 | |
if (count($articles) === 0) { | |
continue; | |
} | |
$html = $this->twig->render('email/author-weekly-report-pdf.html.twig', [ | |
'articles' => $articles, | |
]); | |
// ... lines 68 - 79 | |
} | |
// ... lines 81 - 83 | |
} | |
} |
To turn that HTML into PDF content, we can say $pdf = $this->pdf->getOutputFromHtml($html)
.
// ... lines 1 - 18 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
// ... lines 21 - 46 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
// ... lines 49 - 53 | |
foreach ($authors as $author) { | |
// ... lines 55 - 64 | |
$html = $this->twig->render('email/author-weekly-report-pdf.html.twig', [ | |
'articles' => $articles, | |
]); | |
$pdf = $this->pdf->getOutputFromHtml($html); | |
// ... lines 69 - 79 | |
} | |
// ... lines 81 - 83 | |
} | |
} |
Cool, right! Behind the scenes, this simple method does a lot: it takes the HTML content, saves it to a temporary file, then executes wkhtmltopdf
and points it at that file. As long as wkhtmltopdf
is set up correctly... and our HTML generates a nice-looking page, it should work!
If all has gone well, the $pdf
variable will now be a string containing the actual PDF content... which we could do anything with, like save to a file or attach to an email. Why, what a wonderful idea!
Adding an Attachment
Adding an attachment to an email... probably looks exactly like you would expect: ->attach()
. The first argument is the file contents - so $pdf
. If you need to attach something big, you can also use a file resource here - like use fopen
on a file and pass the file handle so you don't need to read the whole thing into memory. The second argument will be the filename for the attachment. Let's uses weekly-report-%s.pdf
and pass today's date for the wildcard: date('Y-m-d')
.
// ... lines 1 - 18 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
// ... lines 21 - 46 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
// ... lines 49 - 53 | |
foreach ($authors as $author) { | |
// ... lines 55 - 67 | |
$pdf = $this->pdf->getOutputFromHtml($html); | |
$email = (new TemplatedEmail()) | |
// ... lines 71 - 74 | |
->context([ | |
// ... lines 76 - 77 | |
]) | |
->attach($pdf, sprintf('weekly-report-%s.pdf', date('Y-m-d'))); | |
$this->mailer->send($email); | |
} | |
// ... lines 82 - 84 | |
} | |
} |
Love it! We're ready to try this thing. Find your terminal and run:
php bin/console app:author-weekly-report:send
As a reminder, even though this looks like it's sending to six authors, it's a lie! It's really looping over 6 possible authors, but only sending emails to those that have written an article within the past 7 days. Because the database fixtures for this project have a bunch of randomness, this might send to 5 users, 2 users... or 0 users. If it doesn't send any emails, try reloading your fixtures by running:
php bin/console doctrine:fixtures:load
If you are so lucky that it's sending more than 2 emails, you'll get an error from Mailtrap, because it limits sending 2 emails per 10 seconds on the free plan. You can ignore the error or reload the fixtures.
In my case, in Mailtrap... yea! This sent 2 emails. If I click on the first one... it looks good... and it has an attachment! Let's open it up!
Oh... ok... I guess it technically worked... but it looks terrible. This definitely did not have Bootstrap CSS applied to it. The question is: why not?
Next, let's put on our debugging hats, get to the bottom of this mystery, and crush this bug.
When running the console command to send the PDFs, I was getting the error: "Failed to load about:blank, with network status code 301 and http status code 0 - Protocol "about" is unknown". It seems to be related to https links being accessed during the rendering of the PDF (my local site uses https).
I found that adding:
$this->pdf->setOption("enable-local-file-access", true);
right before the line that contains:
$pdf = $this->pdf->getOutputFromHtml($html);
solved the problem!
I hope this helps anyone else that runs in the same problem. And a huge thanks to Ryan and Victor for these great tutorials!!