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 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.
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"aws/aws-sdk-php": "^3.87", // 3.110.11
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
"knplabs/knp-time-bundle": "^1.8", // v1.9.1
"league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
"league/flysystem-cached-adapter": "^1.0", // 1.0.9
"league/html-to-markdown": "^4.8", // 4.8.2
"liip/imagine-bundle": "^2.1", // 2.1.0
"nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
"oneup/flysystem-bundle": "^3.0", // 3.1.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.4.1
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.3.4
"symfony/console": "^4.0", // v4.3.4
"symfony/flex": "^1.9", // v1.9.10
"symfony/form": "^4.0", // v4.3.4
"symfony/framework-bundle": "^4.0", // v4.3.4
"symfony/mailer": "4.3.*", // v4.3.4
"symfony/messenger": "4.3.*", // v4.3.4
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.3.4
"symfony/sendgrid-mailer": "4.3.*", // v4.3.4
"symfony/serializer-pack": "^1.0", // v1.0.2
"symfony/twig-bundle": "^4.0", // v4.3.4
"symfony/twig-pack": "^1.0", // v1.0.0
"symfony/validator": "^4.0", // v4.3.4
"symfony/web-server-bundle": "^4.0", // v4.3.4
"symfony/webpack-encore-bundle": "^1.4", // v1.6.2
"symfony/yaml": "^4.0", // v4.3.4
"twig/cssinliner-extra": "^2.12", // v2.12.0
"twig/extensions": "^1.5", // v1.5.4
"twig/inky-extra": "^2.12" // v2.12.0
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/browser-kit": "4.3.*", // v4.3.5
"symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
"symfony/dotenv": "^4.0", // v4.3.4
"symfony/maker-bundle": "^1.0", // v1.13.0
"symfony/monolog-bundle": "^3.0", // v3.4.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
"symfony/profiler-pack": "^1.0", // v1.0.4
"symfony/var-dumper": "^3.3|^4.0" // v4.3.4
}
}