How to transform ConfigureShowFields to generate PDF in sonata admin bundle? - php

this is my ConfigureShowFields using sonata bundle :
I want to transform my data in ConfigureShowFields to pdf , is possible ??

Its possible using
https://github.com/KnpLabs/KnpSnappyBundle
Firstly install and configure the KnpSnappyBundle
Then:
create custom action in your admin:
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('pdf', $this->getRouterIdParameter().'/pdf');
}
Create the logic for this action in the admin controller
public function pdfAction(Request $request)
{
$id = $request->get($this->admin->getIdParameter());
$object = $this->admin->getObject($id);
if (!$object) {
throw $this->createNotFoundException(sprintf('unable to find the object with id : %s', $id));
}
$this->admin->checkAccess('show', $object);
$this->admin->setSubject($object);
$response = $this->render(
'#App/Admin/pdf.html.twig',
[
'action' => 'print',
'object' => $object,
'elements' => $this->admin->getShow(),
],
null
);
$cacheDir = $this->container->getParameter('kernel.cache_dir');
$name = tempnam($cacheDir.DIRECTORY_SEPARATOR, '_print');
file_put_contents($name, $response->getContent());
$hash = base64_encode($name);
$options['viewport-size'] = '769x900';
$url = $this->container->get('router')->generate('app_print', ['hash' => $hash], Router::ABSOLUTE_URL);
$pdf = $this->container->get('knp_snappy.pdf')->getOutput($url, $options);
return new Response(
$pdf,
200,
[
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'filename="show.pdf"',
]
);
}
Create the pdf view to render the show without admin menu or headers.
App/Admin/pdf.html.twig
{% extends '#SonataAdmin/CRUD/base_show.html.twig' %}
{% block html %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
{% block head %}
{{ parent() }}
{% endblock %}
<body style="background: none">
{% block sonata_admin_content %}
{{ parent() }}
{% endblock %}
</body>
</html>
{% endblock %}
Create a print controller in the app.
/**
* #Route(path="/core/print")
*/
class PrinterController
{
/**
* #Route(path="/{hash}", name="app_print")
*
* #param string $hash
*
* #return Response
*/
public function indexAction($hash)
{
$file = base64_decode($hash);
if (!file_exists($file)) {
throw new NotFoundHttpException();
}
$response = new Response(file_get_contents($file));
unlink($file);
return $response;
}
}
NOTE: This controller is used to use knpSnappy with a url instead of string, in order to avoid conflicts with assets, e.g. images etc. If you don't need print images or styles, simply use $this->get('knp_snappy.pdf')->generateFromHtml() to generate the pdf from the response instead of send to another url and remove the part when is used the cache to create a temporal rendered file.

Related

No block "form" found while rendering the form. in Symfony App outside controller in separate class

i try to create some kind of formbuilder that outputs html of generated forms using the symfony form extenstion (using all the nice stuff like valdation, error hightlighting and such).
is use symfony 5 with twig 3.0
the created class
class FormBuilder{
private $twig;
private $formFactory;
public function __construct()
{
$defaultFormTheme = 'contact.html.twig';
$loader = new FilesystemLoader($_SERVER['DOCUMENT_ROOT'].'../template/forms');
$this->twig = new Environment($loader, ['cache' => $_SERVER['DOCUMENT_ROOT'].'../var/cache/'.$_ENV['APP_ENV'].'/twig']);
$formEngine = new TwigRendererEngine([$defaultFormTheme], $this->twig);
$this->twig->addRuntimeLoader(new FactoryRuntimeLoader([
FormRenderer::class => function () use ($formEngine) {
return new FormRenderer($formEngine);
},
]));
$this->twig->addExtension(new FormExtension());
$this->formFactory = Forms::createFormFactoryBuilder()
->getFormFactory();
}
/**
* #return string
*/
public function getContactForm():string
{
$form = $this->formFactory->createBuilder()
->add('content', TextareaType::class)
->getForm();
return $this->twig->render('contact.html.twig', [
'contact_form' => $form->createView(),
]);
}
}
and call it somewhere else with
$fb = new FormBuilder();
var_dump($fb->getContactForm());
it doesn't matter if the template looks like
{{ form_start(contact_form) }}
{{ form_widget(contact_form) }}
<input type="submit"/>
{{ form_end(contact_form) }}
or
{{ form(contact_form) }}
there is always the and runtime error:
An exception has been thrown during the rendering of a template ("No block "form" found while rendering the form.").
or in first template example instead of "form" it yells about "form_start".
searching for hours now, but seems i to blind to find the missing spot..
any suggestions or tips how to include the form function in twig outside the symfony controller?
with adding themes and (because of translation in the themes) translation it now works
public function __construct()
{
// name of theme template
$defaultFormTheme = 'form_div_layout.html.twig';
$appVariableReflection = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable');
$vendorTwigBridgeDirectory = dirname($appVariableReflection->getFileName());
//where the forms reside
$viewsDirectory = realpath($_SERVER['DOCUMENT_ROOT'].'../template/forms');
//init twig with directories
$this->twig = new Environment(new FilesystemLoader([
$viewsDirectory,
$vendorTwigBridgeDirectory.'/Resources/views/Form',
]));
//apply theme to twig/renderer
$formEngine = new TwigRendererEngine([$defaultFormTheme], $this->twig);
$this->twig->addRuntimeLoader(new FactoryRuntimeLoader([
FormRenderer::class => function () use ($formEngine) {
return new FormRenderer($formEngine);
},
]));
//add form extenstion
$this->twig->addExtension(new FormExtension());
// this is for the filter |trans
$filter = new TwigFilter('trans', function ($context, $string) {
return Translation::TransGetText($string, $context);
}, ['needs_context' => true]);
// load the i18n extension for using the translation tag for twig
// {% trans %}my string{% endtrans %}
$this->twig->addFilter($filter);
$this->twig->addExtension(new Translation());
//prepare factory for later use
$this->formFactory = Forms::createFormFactoryBuilder()
->getFormFactory();
}
just needs additional php extension 'gettext' and the translation extension for twig from https://github.com/JBlond/twig-trans (the original seems to support only twig up to v2.x)
thanks again Jakumi for the solving hint :)

twig: rendering child templates first, then passing to parent template

I am new to Twig and need to check whether the way I use it in my MVC is the 'correct' way. I have a feeling that it isn't;
I want to have a controller for each region in my site and have each controller render their own twig template. I read about including twig templates inside twig templates such as:
main.twig
{% include 'header.twig' %}
{% include 'menu.twig' %}
{% include 'content.twig' %}
{% include 'footer.twig' %}
The problem with this is that I cannot run a separate controller for each region before the template is included. I would have to pass the variables for all regions as once to main.twig and I don't like to do that.
So I now do something like the following:
$regions=[];
//...preprocessing menu items here in a controller...
$template=$twig->loadTemplate('regions/menu.twig');
$regions['menu'] = $template->render(array(
'home' => 'Go to Home',
'contact' => 'Contact page'
));
//...other regions...
$template=$twig->loadTemplate('main.twig');
echo $template->render([
'regions'=>$regions
]);
And regions inside main.twig are then printed using the raw value: {{regions.menu|raw}}
This way I have full control over the data that is passed to each template which is what I want. However I have the feeling that I am now not using Twig the way it is supposed to, because I am saving rendered html in variables and then rendering it again.
If what I am trying to achieve is possible in a better way, please let me know.
I'm thinking it's causing a lot of overhead as you always will need to copy/paste the regions whenever you want to create a new page/controller. Idealy would be to use a main template with the includes and let your views extend from the base one.
base.twig.html
<!DOCTYPE html>
<html>
<head>
<title>{{ page.title | default('') }}</title>
<link rel="stylesheet" type="text/css" href="default.css" />
{% block css %}
{% endblock %}
</head>
<body>
{% block nav %}
<nav id="main">
{% for link in main.links %}
{{ link.title }}
{% endfor %}
</nav>
{% endblock %}
<div id="content">
{% block content %}
{% endblock %}
</div>
{% block javascript %}
{% endblock %}
</body>
</html>
{% extends "base.twig.html" %}
{% block content %}
<h1>{{ title }}</h1>
{% endblock %}
If you want to have a controller for each region you could create a helper class which calls all the controllers you need a returning an multi-dimensional array defined by the class name of the region.
This way your variables will never collide as you can access them by e.g. main.title / menu.title / title
(code is just pseudo-code, did not test/run it, just to give you an idea)
<?php
$regions = (new \Project\Regions\Container())->addRegion('Main')
->addRegion('Menu');
echo $twig->render('child.html', array_merge($regions->getParameters(), [
'title' => 'Hello World',
]);
class Container {
private $regions = [];
public function __construct($regions = []) {
$this->regions = $regions;
}
public function setRegions($regions = []) {
$this->regions = $regions;
return $this;
}
public function addRegion($region) {
if (!in_array($region, $this->regions)) $this->regions[] = $region;
return $this;
}
public function getParameters() {
$data = [];
foreach($this->regions as $region) {
$class = '\Project\Regions\\'.$region;
if (!class_exists($class)) continue;
$data[strtolower($region)] = (new $class())->getParameters();
}
return $data;
}
}
<?php
namespace Project\Regions;
abstract class Region {
public function getParameters() {
return [];
}
}
<?php
namespace Project\Regions;
class Page extends Region {
public function getParamters() {
return [
'title' => 'foo',
];
}
}
<?php
namespace Project\Regions;
class Menu extends Region {
return [
'title' => 'bar',
];
}

howto handle edit forms with FileType inputs in symfony2

in a symfony2 application a entity Message has a one-to-many relation to documents. Documents represents user uploads. i created a form. I realized two Forms: MessageForm and DocumentForm. DocumentForm lives inside a collection FormField in MessageForm. Uploading and processing files does work.
But if i want to edit the entity Message the Form contains as many empty FileInputs as there are Documents existing. desired behaviour would be:
FileInputs to upload new files
Filename (link) to existing files
Possibility to delete existing files
This should be handled inside the form. Changes should be done when the form is submitted.
How can this be realized?
Solution is to write a custom form type extension. as described on http://symfony.com/doc/2.1/cookbook/form/create_form_type_extension.html.
filetype extension
<?php
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* Class FileTypeExtension
*
* #see http://symfony.com/doc/2.1/cookbook/form/create_form_type_extension.html
*/
class FileTypeExtension extends AbstractTypeExtension
{
/**
* Returns the name of the type being extended.
*
* #return string The name of the type being extended
*/
public function getExtendedType()
{
return 'file';
}
/**
* Add the image_path option
*
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setOptional(array('file_path', 'file_name'));
}
/**
* Pass the image url to the view
*
* #param FormView $view
* #param FormInterface $form
* #param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (array_key_exists('file_path', $options)) {
$parentData = $form->getParent()->getData();
if (null !== $parentData) {
$propertyPath = new PropertyPath($options['file_path']);
$fileUrl = $propertyPath->getValue($parentData);
} else {
$fileUrl = null;
}
$view->set('file_url', $fileUrl);
}
if (array_key_exists('file_name', $options)) {
$parentData = $form->getParent()->getData();
if (null !== $parentData) {
$propertyPath = new PropertyPath($options['file_name']);
$fileName = $propertyPath->getValue($parentData);
} else {
$fileName = null;
}
$view->set('file_name', $fileName);
}
}
}
customized file_widget
{% block file_widget %}
{% spaceless %}
{% if file_url is not null %}
<div>{{ file_name }}</div>
<div style="display:none">{{ block('form_widget') }}</div>
{% else %}
{{ block('form_widget') }}
{% endif %}
{% endspaceless %}
{% endblock %}
services.yml
parameters:
foobar.file_type_extension.class: Foobar\Form\Extension\FileTypeExtension
services:
foobar.file_type_extension:
class: %replacethis.file_type_extension.class%
tags:
- { name: form.type_extension, alias: file }
inside a formtype
$builder->add('file','file', array(
"label" => "Datei",
"required" => true,
"attr" => array(),
"file_path" => "webPath",
"file_name" => "name"
));
that's it ;)
In addition to the above answere - wich will let you build a file input wich can be rendered as a link (if it has an url) or a field (if it doesn't) - you might take a look at
http://symfony.com/doc/2.0/cookbook/form/form_collections.html
Wich in conjunction with some jQuery will let you add fields in UI.

symfony form : how to show the parameter in the view, "variable does not exist..."

I'm using a form with 2 classes ("ArticleType" and "ArticleHandler") for my class Article.
I would like to send the id of the article I've just created, but I can't manage to show this id parameter in the view :
In the controller, I send my article's id :
$handler = new ArticleHandler($form, $request, $em);
if ($handler->process()){
return $this->redirect($this->generateUrl('myproject_show', array('id' => $article->getId())) );
}
and in the view, I've got an error with :
{% block body %}
<p>the id :</p>
{{ id }}
{% endblock %}
or entity.id (as in the CRUD) :
Variable "id" does not exist...
Variable "entity.id" does not exist...
Do you know how to fix this?
Thanks
EDIT :
here's my method :
public function addAction()
{
$article = new Article();
$form = $this->createForm(new ArticleType(), $article);
$request = $this->getRequest();
$em = $this->getDoctrine()->getEntityManager();
$handler = new ArticleHandler($form, $request, $em);
if ($handler->process()){
return $this->redirect($this->generateUrl('myproject_show', array('id' => $article->getId())) );
}
return $this->render('ProjBlogBundle:Blog:add.html.twig', array(
'form' => $form->createView(),
));
}
and here's the view :
{% extends "ProjBlogBundle::layout.html.twig" %}
{% block title %}
the title - {{ parent() }}
{% endblock %}
{% block sousbody %}
<p>here's the article i've just created :</p>
{{ id }}
{% endblock %}
EDIT N°2 :
myproject_show:
pattern: /show/{id}
defaults: { _controller: ProjBlogBundle:Blog:show, id:5 }
To use a variable in a template you need to pass it when you render your template:
//ProjBlogBundle:Blog:show
public function showAction($id)
{
return $this->render('ProjBlogBundle:Blog:show.html.twig', array(
'id' => $id
));
}
$this->redirect($this->generateUrl('myproject_show', array('id' => $article->getId())) ); returns only HTTP 302-response without rendering a template, and the browser is redirected to the generated url...

How to change view-parameter within controller?

This is the code that gets executed (as displayed in "code behind this page" section):
Controller Code
/**
* #Route("/hello/{name}", name="_demo_hello")
* #Template()
*/
public function helloAction($name)
{
$name = "whatever";
$this->render('AcmeDemoBundle:Demo:hello.html.twig',
array('name' => '123'));
//return array('name' => 'abc');
}
Template Code
{% extends "AcmeDemoBundle::layout.html.twig" %}
{% block title "Hello " ~ name %}
{% block content %}
<h1>xHello {{ name }}!</h1>
{% endblock %}
The output is
xHello Raffael!
The URL: http://192.168.177.128/Symfony/web/app_dev.php/demo/hello/Raffael
And here is my problem:
When I uncomment the return within controller then "Raffael" is replaced with "abc" as expected.
But according to the Quicktour it is possible to determine the values of variables within the template via the render-Method.
To render a template in Symfony, use the render method from within a
controller and pass it any variables needed in the template:
$this->render('AcmeDemoBundle:Demo:hello.html.twig', array( 'name' =>
$name, ));
What's wrong?
Further down the quick tour implicitely reveales that you have to return the output of render:
public function helloAction($name)
{
return $this->render('AcmeDemoBundle:Demo:hello.html.twig',
array('name' => '123'));
}

Categories