I want to integrate a PDF export into my SONATA ADMIN application. For that, I installed the KnpSnappy Bundle and SonataExporterBundle.
I followed an old tutorial found on google, but in the end it does not work.
config.yml:
knp_snappy:
pdf:
enabled: true
binary: /usr/local/bin/wkhtmltopdf
options: []
image:
enabled: true
binary: /usr/local/bin/wkhtmltoimage
options: []
temporary_folder: %kernel.cache_dir%/snappy
services.yml:
sonata.admin.exporter:
class: AppBundle\Export\Exporter
calls:
- ["setKnpSnappyPdf", ["#knp_snappy.pdf"]]
- ["setTemplateEngine", ["#templating"] ]
in my ModelAdmin, I added it:
public function getExportFormats() {
return array_merge(parent::getExportFormats(), array('pdf'));
}
I created AppBundle/Export/Exporter.php:
namespace AppBundle\Export;
use Exporter\Source\SourceIteratorInterface;
use Symfony\Component\HttpFoundation\Response;
use Sonata\AdminBundle\Export\Exporter as BaseExporter;
class Exporter extends BaseExporter
{
protected $knpSnappyPdf;
protected $templateEngine;
public function getResponse($format, $filename, SourceIteratorInterface $source)
{
if ('pdf' != $format) {
return parent::getResponse($format, $filename, $source);
}
$html = $this->templateEngine->renderView('AppBundle:Export:pdf.html.twig', array(
'source' => $source
));
$content = $this->knpSnappyPdf->getOutputFromHtml($html);
return new Response($content, 200, array(
'Content-Type' => 'application/pdf',
'Content-Disposition' => sprintf('attachment; filename=%s', $filename)
));
}
public function setKnpSnappyPdf($service)
{
$this->knpSnappyPdf = $service;
}
public function setTemplateEngine($service)
{
$this->templateEngine = $service;
}
}
error:
RuntimeException:
Invalid "pdf" format, supported formats are : "csv, json, xls, xml"
at vendor/sonata-project/exporter/src/Exporter.php:52
at Exporter\Exporter->getResponse('pdf', 'export_model_2018_04_20_15_44_05.pdf', object(DoctrineORMQuerySourceIterator))
(vendor/sonata-project/admin-bundle/src/Controller/CRUDController.php:952)
at Sonata\AdminBundle\Controller\CRUDController->exportAction(object(Request))
(vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:151)
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
(vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:68)
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
(vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php:202)
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
(web/app_dev.php:31)
at require('/var/www/html/acianovintra/web/app_dev.php')
(vendor/symfony/symfony/src/Symfony/Bundle/WebServerBundle/Resources/router.php:42)
Can you tell me what I did wrong?
In a project I had to export some data in docx format.
To do that, I wrote a custom writer :
<?php
namespace App\MyBundle\Utils\Exporter\Writer;
use Exporter\Writer\TypedWriterInterface;
class DocxWriter implements TypedWriterInterface
{
/**
* {#inheritdoc}
*/
final public function getDefaultMimeType()
{
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
}
/**
* {#inheritdoc}
*/
final public function getFormat()
{
return 'docx';
}
//...
}
Then I had to create a service for the writer and add a tag :
// service.xml
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="app.exporter.writer.docx" class="App\MyBundle\Utils\Exporter\Writer\DocxWriter">
<argument>php://output</argument>
<tag name="sonata.exporter.writer"/>
</service>
</services>
</container>
In the ExporterCompilerPass of the exporter bundle, sonata get all the services tagged with sonata.exporter.writer and pass them to the addWriter of the Exporter class.
To integrate this new writer in sonata admin, you'll have to override the sonata.admin.exporter like you already did, except that you will call this new writer :
<?php
namespace App\MyBundle\Export;
use Exporter\Source\SourceIteratorInterface;
use Sonata\AdminBundle\Export\Exporter as BaseExporter;
use App\MyBundle\Utils\Exporter\Writer\DocxWriter;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Exporter extends BaseExporter
{
public function getResponse($format, $filename, SourceIteratorInterface $source)
{
switch ($format) {
case 'docx':
$writer = new DocxWriter('php://output');
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
break;
default:
return parent::getResponse($format, $filename, $source);
}
$callback = function () use ($source, $writer) {
$handler = \Exporter\Handler::create($source, $writer);
$handler->export();
};
return new StreamedResponse($callback, 200, array(
'Content-Type' => $contentType,
'Content-Disposition' => sprintf('attachment; filename="%s"', $filename),
));
}
}
With this, you should be able to add your pdf format in your admin overriding the getExportFormats method.
Hpe this helps
Related
I have an UserEventListener which implements EventListenerInterface.
In the event: afterLogin($event){} I would like to redirect the user if the profile is not complete, I can't figure out how to do redirect with the $event parameter
Edit:
here is the code of the UserEventListener:
<?php
namespace App\Event;
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use Cake\Event\EventListenerInterface;
use Cake\Datasource\FactoryLocator;
use Cake\Log\Log;
class UserEventListener implements EventListenerInterface
{
public function implementedEvents(): array
{
return [
'Users.Authentication.afterLogin' => 'afterLogin',
//'Users.Authentication.afterLogout' => 'afterLogout'
];
}
public function afterLogin($event)
{
$profilesTable = FactoryLocator::get('Table')->get('Profiles');
$profiles = $profilesTable->find()
->where(['user_id' => $event->getData('user')->id])
->order(['id' => 'DESC'])->toList();
if((count($profiles) == 1) && $profiles[0]->mobile == "" ){
//-- here I would like to redirect --------
//return $event->getSubject()->redirect(['Controller'=>'Profiles', 'action' => 'edit',$event->getData('user')->id]);
}
}
}
Prior to CakePhp3.7 it was possible to load a plugin using the autoload option:
Plugin::load('ContactManager', ['autoload' => true]);
This was very useful if you couldn't (or didn't want to) use composer to autoload plugins.
Since version 3.7.0: Plugin::load() and autoload option are deprecated.
$this->addPlugin('ContactManager');
Should be used instead of Plugin::load. But autoload option is not available on addPlugin().
How can I replicate the autoload functionality in CakePhp3.7 without using composer?
Well, there isn't much you can do other than re-implementing/replicating what Plugin::load() does, that is registering an autoloader, see:
https://github.com/cakephp/cakephp/blob/3.7.8/src/Core/Plugin.php#L130-L142
https://github.com/cakephp/cakephp/blob/3.7.8/src/Core/Plugin.php#L157-L170
You could for example put it in your Application class:
use Cake\Core\ClassLoader;
use Cake\Core\Plugin;
// ...
class Application extends BaseApplication
{
// ...
protected static $_loader;
public function bootstrap()
{
// ...
$this->addPlugin('ContactManager', ['autoload' => true]);
}
public function addPlugin($name, array $config = [])
{
parent::addplugin($name, $config);
$config += [
'autoload' => false,
'classBase' => 'src',
];
if ($config['autoload'] === true) {
if (!isset($config['path'])) {
$config['path'] = Plugin::getCollection()->findPath($name);
}
if (empty(static::$_loader)) {
static::$_loader = new ClassLoader();
static::$_loader->register();
}
static::$_loader->addNamespace(
str_replace('/', '\\', $name),
$config['path'] . $config['classBase'] . DIRECTORY_SEPARATOR
);
static::$_loader->addNamespace(
str_replace('/', '\\', $name) . '\Test',
$config['path'] . 'tests' . DIRECTORY_SEPARATOR
);
}
return $this;
}
// ...
}
For now \Cake\Core\ClassLoader isn't deprecated, but it might become at one point, so that you may have to re-implement that too.
I'm working on CakePHP 3.4
I have a contact_messages table to save message via form on website.
I want to send user an email whenever a new message is saved.
For that, I have created mailer class like
<?php
namespace App\Mailer;
use Cake\Mailer\Mailer;
use Cake\Event\Event;
use Cake\Datasource\EntityInterface;
class ContactMessageMailer extends Mailer
{
public function newMessage($message)
{
$this
->setProfile('no-reply')
->setTemplate('new_message')
->setLayout('message')
->setEmailFormat('html')
->setTo($user->email) // user email
->setSubject('Verify Account')
->setViewVars(['name' => $user->first_name, 'email' => $user->email, 'message' => $message->body]);
}
public function implementedEvents()
{
return [
'Model.afterSave' => 'alertMessage'
];
}
public function alertMessage(Event $event, EntityInterface $entity, \ArrayObject $options)
{
if ($entity->isNew()) {
$this->send('newMessage', [$entity]);
}
}
}
and registering event in ContactMessagesTable.php
$mailer = new UserMailer(); //use App\Mailer\UserMailer;
$this->eventManager()->on($mailer);
ContactMessages belongsTo Users and Users is having email of user whom to send the email.
How can I get users information in Mailer?
Will probably do this;
In the User table;
public function processUser($user)
{
if($this->save($user)){
$event = new Event('Model.afterSave', $this, [$entity = $user])
$this->eventManager()->dispatch($event);
return true;
}else{
return false;
}
}
In ContactMessage Table ;
public function initialize()
{
parent::intialize();
$mailer = new UserMailer(); //use App\Mailer\UserMailer;
$this->Users->eventManager()->on($mailer); //ContactMessage has to be related to User table
}
Hope I was able to communicate.
I have problem, i can't return my posts array to json becouse symfony returns array with entity object?
Its my code:
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$posts = $em->getRepository('AppBundle:Post')->findAll();
return $this->json($posts);
}
I use $this->json is return json data, feature added on sf3.
But this is my result:
[
{},
{},
{}
]
i want to load my posts.
ps. i know, i can use Query builder, and method toArray or something, but is any method to use and DRY? Thx
Because entity can have multiple boundaries, proxy objects and related entities, I personally prefer to explicitly specify what is about to be serialized, like this:
use JsonSerializable;
/**
* #Entity
*/
class SomeEntity implements JsonSerializable
{
/** #Column(length=50) */
private $title;
/** #Column(length=50) */
private $text;
public function jsonSerialize()
{
return array(
'title' => $this->title,
'text' => $this->text,
);
}
}
And then it's as simple as json_encode($someEntityInstance);.
You can use JMSSerializerBundle as well to accomplish your task DRY.
Also, there is an option to write your own serializer to normalize the data.
UPDATE:
If you want multiple representations of a JSON, it can be achieved like this:
use JsonSerializable;
/**
* #Entity
*/
class SomeEntity implements JsonSerializable
{
// ...
protected $isList;
public function toList()
{
$this->isList = TRUE;
return $this;
}
private function jsonSerializeToList()
{
return [ // array representing list... ]
}
public function jsonSerialize()
{
if( $this->isList ) {
$normalized = $this->jsonSerializeToList();
} else {
$normalized = array(
'title' => $this->title,
'text' => $this->text,
);
}
return $normalized;
}
}
And called as json_encode($someEntityInstance->toList());. Any way, this is a bit dirty, so I suggest to be consistent with an idea of the interface.
A best solution is to enable the serializer component in Symfony:
#app/config/config.yml
framework:
serializer: ~
Note: the serializer component is disabled by default, you have to uncomment the config line in app/config/config.yml file.
I try to do a contact form with Symfony 2.4.1. My form is a simple contact one without an entity.
The following error happens in my handler :
FatalErrorException: Error: Call to undefined method Symfony\Component\Form\Form::render() in C:\Program Files\wamp\www\sf2\src\Open\OpcBundle\Form\Handler\ContactHandler.php line 75
And the form handler code :
<?php
// src/Open/OpcBundle/Form/Handler/Handler.php
namespace Open\OpcBundle\Form\Handler;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
class ContactHandler {
protected $request;
protected $form;
protected $mailer;
public function __construct(Form $form, Request $request, $mailer) {
$this->form = $form;
$this->request = $request;
$this->mailer = $mailer;
}
public function process() {
if ('POST' == $this->request->getMethod()) {
$this->form->handleRequest($this->request);
$data = $this->form->getData();
$this->onSuccess($data);
return true;
}
return false;
}
protected function onSuccess($data) {
$message = \Swift_Message::newInstance()
->setContentType('text/html')
->setSubject($data['sujet'])
->setFrom($data['courriel'])
->setTo('me#gmail.com')
->setBody($this->render('OpcOpenBundle:Opc:Mails/contact.html.twig',
array('ip' => $request->getClientIp(),
'nom' => $data['nom'],
'msg' => $data['msg'])
)
);
$this->get('mailer')->send($message);
$request->getSession()->getFlash()->add('success', 'Your email has been sent! Thanks!');
return $this->redirect($this->generateUrl('contact'));
}
}
So the problem is that I don't have an object/instance for the renderView() method to call the template mail with setBody in the function onSuccess()
Which object/instance should I use for ?
Thanks
PS: sorry for my english, i'm French!
It's Symfony\Bundle\TwigBundle\TwigEngine class. service name is 'templating'.
BTW, why is you handler is not service?