Hola, he buscado mucho por internet la forma de generar archivos PDF desde una aplicación Symfony2.
Debe ser utilizando la librería FPDF, pues hay algunos archivos PDF ya codificados con esta librería y la idea seria migrarlos. Ademas no puedo usar ninguna librería de tipo HTML to PDF porque necesito crear informes de este tipo
Como verán son muy complejos, y con FPDF pues ya me es relativamente fácil hacerlos. ¿Conocen algún Bundle para este propósito? ¿o debo usar la clase FPDF directamente?
Por otra parte, debería activar el sistema de plantillas PHP? ¿o debería crear clases PHP?
Espero puedan orientarme y ayudarme, gracias.
Respuestas
No conozco ningún bundle de Symfony para integrar la librería FPDF ... pero tampoco estoy seguro de que te fuera a ayudar en nada importante.
Si la librería FPDF ya te genera todo el PDF y sabes cómo usarla, sólo tienes que crear un servicios al que le pases la información y ya te devuelva el archivo PDF o la ruta del archivo PDF que ha generado en algún lugar.
Si tienes dudas o preguntas sobre cómo crear este servicio, dínoslo y te damos alguna pista más.
@javiereguiluz
Hola Javier, gracias por tu respuesta.
Bueno, en principio si supuse que debía usar la clases FPDF independientemente, me ayudaría mucho me explicaras como hacer lo del servicio que mencionas.
Mi otra gran duda es la organización de estos archivos (código PHP) que genera los reportes.
Aquí te muestro un ejemplo básico de la generación de un PDF usando esta librería.
Como puedes ver en el código, hay una clase para imprimir el header y el footer y el resto del código para completar el documento PDF.
No se en qué directorio guardar estos archivos, para el usuario final supone una vista (a este se le muestra el reporte). Pero como ves el código no es de una vista común.
Espero me entiendas.
Gracias.
@ramiroanacona
Como es imposible dar una respuesta detallada, te indico a continuación a grandes rasgos los pasos a seguir:
1. El controlador
// src/AppBundle/Controller/ReportController.php use AppBundle\Entity\Invoice; class ReportController extends Controller { /** * @Route("/informe/{id}", name="report") */ public function index(Invoice $invoice) { $pdf = $this->get('app.invoice_printer')->toPdf($invoice); // devolver un BinaryResponse() para forzar a descargar el PDF // devolver un Response() normal para visualizar el PDF en el navegador } }
Simplemente utilizamos un servicio (app.invoice_printer
) para generar el archivo PDF a partir de los datos de una factura ($invoice
) cuyos datos obtenemos de la base de datos.
2. Servicios de la aplicación
# app/config/services.yml app.invoice_pdf: class: AppBundle\Printer\InvoicePdf app.invoice_printer: class: AppBundle\Printer\InvoicePrinter arguments: [@invoice_pdf]
Definimos un servicio app.invoice_pdf
para configurar todo lo necesario de la clase base utilizada para generar las facturas (esto es algo muy relacionado con FPDF). Después, definimos otro servicio app.invoice_printer
que es algo totalmente propio de nuestra aplicación y por tanto, no depende directamente de la librería que utilicemos.
3. La clase InvoicePdf
// src/AppBundle/Printer/InvoicePdf.php class InvoicePdf extends FPDF { function Header() { // ... } function Footer() { // ... } }
Aquí es donde colocas todo el código común de FPDF.
4. La clase InvoicePrinter
// src/AppBundle/Printer/InvoicePrinter.php class PdfPrinter { private $pdf; public function __construct(InvoicePdf $pdf) { $this->pdf = $pdf; } public function toPdf(Invoice $invoice) { $this->pdf->AliasNbPages(); $this->pdf->AddPage(); $this->pdf->SetFont('Times','',12); for($i=1;$i<=40;$i++) { $this->pdf->Cell(0,10,'Printing line number '.$i,0,1); } return $this->pdf; } }
Esta es la clase que realmente genera el archivo PDF final utilizando la clase InvoicePdf
auxiliar creada anteriormente.
@javiereguiluz
Gracias Javier, voy aclarando las dudas. Antes de pasar a las pruebas quiero hacerte una pregunta mas.
En la clase InvoicePdf
extiende de FPDF
, pero no veo en que parte estas incluyendo o vinculando la clase FPDF, o en que parte le dices al servicio que incluya la clase FPDF
.
Gracias
@ramiroanacona
Si la librería FPDF la instalas mediante Composer, las clases se cargan automáticamente. Sólo tienes que añadir el use
adecuado según sea el namespace de la clase que quieres cargar. ejemplo:
// src/AppBundle/Printer/InvoicePdf.php use fpdf\FPDF; class InvoicePdf extends FPDF { function Header() { // ... } function Footer() { // ... } }
Si la librería es tan vieja y mala que no se puede instalar con Composer, entonces te tocará configurar a mano cómo es el autoload de esa librería. Eso se hace en el archivo app/autoload.php
de la aplicación Symfony y en este artículo tienes más información sobre cómo hacerlo.
@javiereguiluz
Hola Javier, ahora me surge una gran duda.
¿Y si son muchos informes, más de 40?
Gracias.
@ramiroanacona
En ese caso, tendrás que crear un servicio genérico (por ejemplo app.printer
) y pasar otro argumento al método toPdf()
para indicarle qué plantilla (o clase de tipo FPDF) utilizar en cada caso:
// código original $pdf = $this->get('app.invoice_printer')->toPdf($invoice); // nuevo código $pdf = $this->get('app.printer')->toPdf($invoice, $template); $pdf = $this->get('app.printer')->toPdf($invoice, $printerClass);
Seguro que hay muchas otras formas de resolverlo. Puedes coger otras ideas por ejemplo de bundles como PdfBundle que crean los PDF directamente a través de plantillas Twig.
@javiereguiluz
Hola Javier.
Bueno, he pensado que puede funcionar de esta manera:
<?php // src/AppBundle/Printers/Basic.php namespace AppBundle\Printers; class Basic extends \FPDF { private $data; public function __construct($data) { parent::__construct(); $this->data = $data; } // Page header public function Header() { // ... } // Page footer public function Footer() { // ... } public function Pdf() { $pdf = $this; $pdf->AddPage(); $pdf->SetFont('times','B',16); $pdf->Cell(40,10, $this->data); // Many complex code here... $pdf->Output(); } }
// src/AppBundle/Controller/ReportController.php // ... use AppBundle\Printers\Basic; class ReportController extends Controller { public function index() { $datos = 'Hola'; // Puede ser un query o cualquier cosa $pdf = new Basic($datos); return new Response($pdf->Pdf()); // devolver un BinaryResponse() para forzar a descargar el PDF // devolver un Response() normal para visualizar el PDF en el navegador } }
Te cuento un poco: no todos los informes tienen la misma configuración como: orientación de la página, márgenes, etc. y esto se hace en el constructor de la clase FPDF (o TCPDF que es una mejora) por esa razón creo que lo mas fácil (teniendo en cuenta lo distinto que es cada formato) es crear una clase por cada reporte.
Espero me des tu opinion al respecto.
Gracias
@ramiroanacona
Si la parte que es diferente de un informe a otro es muy compleja, creo que sí que habría que hacer una clase propia para cada tipo de informe. Si las diferencias son importantes, pero se limitan a una serie de opciones de configuración, creo que podrías añadir un argumento adicional para pasar esas opciones.
Otro comentario: en Symfony puedes crear servicios mediante factorías. Esto significa que podrías crear una sola factoría PHP capaz de crear cada una de las clases asociadas a los informes. Así podrías tener tantos servicios como necesites, sin que te cueste mucho tiempo crearlos.
@javiereguiluz
Hola Javier.
Me parece bueno eso de crear varios servicios fácilmente, pero no termino de entenderlo. Sería mucho pedir si lo explicaras con un ejemplo, teniendo en cuenta lo siguiente:
Hay varias clases, de las cuales cada una debe imprimir un reporte en PDF, puede ser:
ReportInvoices
ReportClients
ReportProducts
Y así sucesivamente muchos mas reportes. Ya entendí cómo crear los servicios independientes y es muy bueno ya que (por ejemplo) no debes especificar la sentencia use
en el controlador.
Gracias.
@ramiroanacona
El ejemplo más típico de las factorías de servicios son los repositorios de Doctrine. En mis aplicaciones Symfony siempre defino un servicio por cada repositorio al que accedo habitualmente:
# app/config/services.yml blog_repository: class: Doctrine\ORM\EntityRepository factory: ["@doctrine.orm.entity_manager", getRepository] arguments: [AppBundle\Entity\BlogPost] comment_repository: class: Doctrine\ORM\EntityRepository factory: ["@doctrine.orm.entity_manager", getRepository] arguments: [AppBundle\Entity\BlogComment] user_repository: class: Doctrine\ORM\EntityRepository factory: ["@doctrine.orm.entity_manager", getRepository] arguments: [AppBundle\Entity\User]
Como ves, todos estos servicios en realidad se crean mediante una única factoría que proporciona Doctrine. Sólo tienes que pasar como argumento la ruta completa de una clase, y obtendrás un servicio que representa a ese repositorio.
En la práctica, esto me permite simplificar mucho el código de los controladores:
$post = $this->get('blog_repository')->findOneBy(['slug' => '...']); $user = $this->get('user_repository')->find(32);
En tu caso, en vez de entidades tendrías informes y en vez de la factoría de Doctrine tendrías la factoría creada por ti mismo. De todas formas, de primeras no te aconsejo que te compliques con esto: crea a mano unos cuantos servicios y cuando tengas claro cómo funciona todo y lo que quieres hacer, entonces sí que te lanzas a crear la factoría de servicios para simplificar el código de la aplicación.
@javiereguiluz
Buenos días. Ramiro, por favor me podrías decir cómo te fue con este tema de FDF en Symfony? Estoy haciendo un proyecto y justo necesito todo lo que preguntabas acá.
Quiero expresar la buena impresión que me causa la diligencia y amabilidad con la que respondió Luis, muchas gracias.
Alexánder Rojas
@J_4lexander