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.
Hey Mark,
Hm, not sure about this, we also use Wkhtmltopdf on SymfonyCasts that works only by HTTPS, and we don't have any problems with generating PDFs.
I noticed 301 status code in the error you mentioned, probably somewhere you're referencing to HTTP but your server/website redirects to HTTPS? I'd recommend you to fix all your links to be HTTPS, then no redirects will be set and PDF generation should work fine even wihtout "enable-local-file-access" I think.
I hope this helps. Anyway, I'm glad you found a working solution for yourself. And thank you for sharing the solution with others!
Cheers!
Hi there.
If you are using the Dompdf library (like me), and it outputs the following message:
No block-level parent found. Not good.
at the end of the foreach (inside its body), you should write the following lines of code:
Hey AbelardoLG,
Hm, this sounds like a workaround: you kill the old instance and create a new one. Sounds like a bug? Anyway, thank you for sharing this tip with others.
Cheers!
Hello,
I was trying the exact same but have the following error:
In DefinitionErrorExceptionPass.php
line 54:
Cannot autowire service "App\Service\MailerServiceStaff": argument "$pdf" of method "__construct()" references class "Knp\Snappy\Pdf" but no such service exists.
I have installed "composer require knplabs/knp-snappy"
But errors remain the same.
when i try to see "debug:autowiring pdf"
It does not show Knp\Snappy\PDF. What is the solution?
I am using contao 4.12 (which is with symfony 5)
Help will be appreciated!
Hey Rana,
I believe you installed the wrong library, you need to install the bundle as mentioned in this chapter https://symfonycasts.com/screencast/mailer/snappy-wkhtmltopdf
Cheers!
To me
{{ encore_entry_link_tags('app') }}
in
author-weekly-report-pdf.html.twig
generated an error when calling php bin/console app:author-weekly-report:send :
exit with code 1 due to network error: Protocolunknownerror .
I found , after lot of search and testings , that the pb was from twig file and I changed it to
{% for path in encore_entry_css_files('app') %}
<link rel="stylesheet" href="{{ absolute_url(path) }}">
{% endfor %}
Now email are sending correctly and without error.
Maybe can be usefull to someone this information.
Hey Stefan,
Hm, that encore_entry_link_tags() function should be valid, that's weird. But I'm happy you were able to find a workaround by manually iterating and render CSS files with encore_entry_css_files(), well done! And thanks for sharing it with others!
Most probably that problem is coming from the Wkhtmkltopdf - it might require absolute URL instead of just a path without the host, probably might depend on its version.
Cheers!
Hi,
Do you know any working Wkhtmltopdf alternative? Such as Weasyprint?
My sysadmin that uses the gentoo linux distribution told me that Wkhtmltopdf is being deprecated because of the qtwebkit dependency, which seems to not be maintained anymore ( https://bugs.gentoo.org/692532 ).
Would be very thankful if someone can share some knowledge about this matter.
Thanks a lot!
Hey Samuel,
We do use Wkhtmltopdf for generating PDFs on SymfonyCasts and it works good for us. Not sure about the deprecated dependency, but it's an issue that should be workaround by Wkhtmltopdf I suppose. Anyway, I don't see any information that the Wkhtmltopdf tool is deprecated, the latest stable release was 0.12.6 on June 11, 2020.
I personally do not have any experience with alternatives to Wkhtmltopdf tools so can't help with this, but it should be easy to find some similar projects on GitHub I suppose. From my side, I can say that Weasyprint sounds interesting, also you could take a look at some others:
- https://github.com/dompdf/d...
- https://github.com/MrRio/jsPDF
- https://github.com/RelaxedJ...
2 latest seems JS solutions, I hope this helps!
Cheers!
Hi Victor,
Sorry for the late response.
We finally used Weasyprint, was easy to implement and can generate pdfs on the fly without creating temporary files.
We are thinking about creating a small vendor bundle wrapping a few command line parameters. If so, I'll let you know for your info.
Thanks a lot!
Hey Samuel,
No problem! Thank you for letting us know what you chose. Fairly speaking, I haven't worked with Weasyprint yet, but that sounds really interesting! And yeah, having a bundle that will make the integration even smoother is awesome, good luck with it! If you could share the link to the bundle later - it would be useful I think ;)
Cheers!
Hi,
I'm using Docker and, for some reason, I get a blank pdf.
I made a public repo should anyone would be so nice to look into it.
https://bitbucket.org/j_bon...
To get the project running, you could fire run.sh on the root directory or alternatively running the commands inside that script manually.
I would really like to get this working.
Best,
- J
Hey Julien,
I'd recommend you to "ssh" into that container, i.e. where you have Wkhtmltopdf installed, and run it manually. You can try to run the default command recommended by Wkhtmltopdf to see if it works as expected:
wkhtmltopdf http://google.com google.pdf
Then try to look into that google.pdf - do you see a Google start page PDF-ed? Also, look closer to the output of that command, it may contain some tips or errors if something is missing.
I hope this helps!
Cheers!
I get an error:
Notice: Object of class Symfony\Component\HttpFoundation\Response could not be converted to number
$html = $this->twig-$this->render('daily_report/showdailypdf.html.twig', [
'daily' => $dailyreport <-----ERROR HERE
]);
$pdf = $this->pdf->getOutputFromHtml($html);
Any thoughts?
Hey Brandon!
Oh wow That's an interesting error :). I think I see the error - and it's SO subtle!
-$html = $this->twig-$this->render('daily_report/showdailypdf.html.twig', [
+$html = $this->twig->$this->render('daily_report/showdailypdf.html.twig', [
See the difference? The "+" line is the one I've corrected - you were missing a tiny >
, which made it look like you were trying to do subtraction (hence the "converted to number" error).
Let me know if this fixes it :).
Cheers!
For Windows 10 Users (if there are others besides me :D):
I had to move the installation of the wkhtmltopdf from Program Files to C:/ so there are not any spaces in the path eg: "Program FIles"
My .env config:
###> knplabs/knp-snappy-bundle ###<br />WKHTMLTOPDF_PATH='C:\wkhtmltopdf\bin\wkhtmltopdf.exe'<br />WKHTMLTOIMAGE_PATH='C:\wkhtmltopdf\bin\wkhtmltoimage.exe'<br />###< knplabs/knp-snappy-bundle ###
It's not needed, you can achieve this by treating C:\Program Files directory in the old MS-DOS wa, where there was 8 character limit per folder/file
so C:\Program Files becomes "C:\Progra~1"
Here's the source (i've checked it on my windows 10 and it works)
https://superuser.com/quest...
```
Note for Windows users on 64-bit systems
Progra~1 = 'Program Files'
Progra~2 = 'Program Files(x86)'
```
Hey Brandon
I'm glad to hear it. If you don't mind could you please tell us how you fixed your problem so others can benefit from your answer?
Cheers!
// 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.17.6
"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
}
}
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!!