TCPDF Example

Retornant un PDF des d'un Controller de Drupal 8

En primer lloc afegim el TCPDF al nostre projecte:

composer require tecnickcom/tcpdf

Creem la ruta i el controller al nostre mòdul, coderscat.routing.yml:

coders.cat.testpdf:
  path: '/testpdf'
  defaults:
    _controller: '\Drupal\coderscat\Controller\PDFController::testPDF'
    _title: 'Test PDF'
  requirements:
    _access: 'TRUE'

Creem la funció testPDF dins el nostre PDFController:

public function testPDF() {
  $pdf = new \TCPDF();
  $pdf->SetAuthor('Coders.cat');
  $pdf->SetTitle('Test browser output');

  $pdf->addPage();
  $pdf->SetFont('helvetica', '', 16);
  $pdf->Cell(0, 100, 'Test pdf content');

  $pdf->Output('test.pdf', 'I');
}

I aparentment tot funciona correctament, veiem el pdf al navegador, i si mirem als missatges de registre del drupal no apareix cap error ni cap notice. Però si mirem als logs del servidor web trobem els següents missatges:

Got error 'PHP message: Uncaught PHP Exception LogicException: "The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?"

Got error ' line 169PHP message: RuntimeException: Failed to start the session because headers have already been sent by "/var/www/coders.cat/vendor/tecnickcom/tcpdf/include/tcpdf_static.php" at line 350.

Ens cal doncs tornar una Response des del Controller, mirant la documentació del mètode Output tenim les següents opcions que podríem provar:

  • I: send the file inline to the browser (default). Aquesta és la que hem utilitzat en l'exemple
  • D: send to the browser and force a file download with the name given by name. Ens trobem amb el mateix error que abans
  • F: save to a local server file with the name given by name. Podríem desar-ho en un fitxer al disc i retornar una response amb l'enllaç, però això ens obre un mar de dubtes i opcions, per exemple nom del fitxer únic?, path?, públic o privat?, permanent o temporal? etc.
  • S: return the document as a string (name is ignored). Desem l'output en un string i l'escrivim directament a la resposta, diria que ens hi acostem, però és un desgast de memòria innecessari (no entrarem en detalls...)
  • La resta d'opcions no ens serveixen tampoc.

Remenant una mica els tipus de response ens topem amb un tal StreamedResponse, la doc diu: A StreamedResponse uses a callback for its content. The callback should use the standard PHP functions like echo to stream the response back to the client.

Mola, bàsicament m'està dient que qualsevol output ;) (veieu per on vaig?) del callback s'envia al client. Ja només ens falta una petita part del puzzle que molts haureu deduït:

php://output is a write-only stream that allows you to write to the output buffer mechanism in the same way as print and echo.

I amb el coneixement adquirit ja podem reescriure la funció anterior de la següent manera:

public function testPDF() {
  $pdf = new \TCPDF();
  $pdf->SetAuthor('Coders.cat');
  $pdf->SetTitle('Test browser output');

  $pdf->addPage();
  $pdf->SetFont('helvetica', '', 16);
  $pdf->Cell(0, 100, 'Test pdf content');

  $headers = [
    'Content-Disposition' => 'inline; filename="test.pdf"',
    'Content-Type' => 'application/pdf'
  ];

  return new StreamedResponse(function() use($pdf) {
    $pdf->Output('php://output', 'F');
  }, Response::HTTP_OK, $headers);
}

Podem posar attachment en lloc de inline si en volem forçar la descàrrega.