PHP Anfängerfrage: PDF-Datei downloaden lassen nach Absenden eines Formulars

Photon

Rear Admiral Pro
🎅 Nikolaus-Rätsel-Elite
Registriert
Apr. 2006
Beiträge
5.207
Hallo Community,

ich bin ziemlicher Anfänger in PHP und frage mich, wie folgendes Verhalten erreicht werden kann:

Der Benutzer füllt ein Formular aus und drückt auf "Submit". PHP generiert dabei eine PDF-Datei (was hier passiert, interessiert nicht weiter), die dem Nutzer zum Download angeboten wird. Im Browser wird dabei dieselbe Seite wieder angezeigt, sodass der Nutzer seine Eingaben ändern und die Datei neu generieren lassen kann.

Im Moment sieht das Formular so aus:


PHP:
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>"> 
   ...
  <input type="submit" name="submit" value="Generieren">  
</form>

Das Generieren der Datei klappt schon via Druck auf "Generieren", sie liegt dann auf dem Server in einem Unterverzeichnis. Die Frage ist nun, wie man die Datei zum Download anbietet. Ich habe Folgende versucht:

PHP:
if ($_SERVER["REQUEST_METHOD"] == "POST") {
  $fp = fopen($pdf_f, 'rb');
  header('Content-Type: application/pdf');
  header('Content-Disposition: attachment; filename="' . $outp_file . '"' );
  header('Content-Length: ' . filesize($pdf_f));
  fpassthru($fp);
}

Allerdings meldet der Browser dann Folgendes:

Alert_001.png



Zudem startet der Download gefühlt zu früh, vielleicht ist die Datei deshalb korrupt. Ist der obige Fünfzeiler nicht auskommentiert, dann schlagen vorangegangene Befehle fehl (echos, die Debug-Infos im <body> auf die Seite schreiben).

Vielleicht wäre es schlauer, einfach einen Download-Link auf die Seite zu setzen? Aber wie handhabt man dann am schlausten den Dateinamen, sodass der Nutzer einen schönen (also nicht zufallsgenerierten) Namen sieht, aber zwei Nutzer sich nicht in die Quere kommen? Und nach dem Runterladen sollte die Datei ja gelöscht werden, damit der Server nicht zugemüllt wird, das geht mit einem statischen Link auch nicht so einfach...

Vielen Dank für alle Tipps!
Photon
 
Teste mal mit sowas

Code:
if(file_exists($pdf_f,)&&is_readable($pdf_f,)){die('file exists');}

ob die Datei vorhanden ist und gelesen werden kann.
 
Jap, er schreibt "file exists", die Datei ist also da. Ich sehe sie ja auch im entsprechenden Ordner auf dem Server.

edit: Nach Hinzufügen dieser Zeile wird aber kein Download mehr angestoßen, der Browser zeigt also kein entsprechendes Fenster.
 
Mit dem Befehl "die()" bricht er sofort ab und macht die Textausgabe. Das muss also wieder raus. Das ist nur zum testen.

Probiere mal, readfile($filename); statt fpassthru()
 
Hab gerade deinen nur sehr leicht modifiziert (test.pdf ist im Verzeichnis) und es geht.
Vll liegts ja an deinem Webserver oder so. Benutz mal den PHP internen webserver.
Also
Bash:
php -S localhost:8000
und guck ob es damit läuft.
PHP:
<form method="post" action="<?= htmlspecialchars($_SERVER['PHP_SELF']); ?>">
   ...
  <input type="submit" name="submit" value="Generieren">
</form>

<?php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  $fp = fopen('test.pdf','rb');
  header('Content-Type: application/pdf');
  header('Content-Disposition: attachment; filename="test.pdf"');
  header('Content-Length: ' . filesize('test.pdf'));
  fpassthru($fp);
}
 
Zuletzt bearbeitet von einem Moderator:
Uff, jetzt laufe ich in andere Schwierigkeiten. Der Server läuft auf einem Raspi im lokalen Netzwerk, wenn ich versuche via 192.168.1.2:8000 darauf zuzugreifen, kann keine Verbindung hergestellt werden. Ist vielleicht ein Sicherheitsmechanismus, der Zugriffe nur von derselben Maschine erlaubt? Vom Raspi aus kann ich leider nicht drauf zugreifen, weil er headless läuft, hab also nur via SSH Zugriff...

Habe nun also den PHP-Server auf meinem Laptop gestartet und kann da die Seite aufrufen, da ist aber nun PHP 8 statt 7 installiert und spuckt Fehler, wo Version 7 noch Warnungen ausspuckte... Ich schau mal, ob ich das zügig repariert bekomme...

Nachtrag: Offenbar nicht... Vielleicht könnt ihr mir bei diesem Problem auch kurz helfen: Ich versuche ein Formular mit mehreren Checkboxen anzulegen, wobei der Zustand der Checkboxen in einem Array gespeichert werden sollte. Laut https://stackoverflow.com/questions/14026361/php-multiple-checkbox-array geht das so:


PHP:
<input type='checkbox' name='checkboxvar[]' value='Option One'>1<br>
<input type='checkbox' name='checkboxvar[]' value='Option Two'>2<br>
<input type='checkbox' name='checkboxvar[]' value='Option Three'>3

Nun meldet PHP 8 aber:

Code:
Uncaught Error: Undefined constant "checkboxvar"

Wenn ich die Variable in einem vorangestellten PHP-Block initialisiere mit


PHP:
$checkboxvar=[];

bringt das aber keine Besserung...
 
Zuletzt bearbeitet:
Jetzt hab ich mit einem Freund geredet und wir sind draufgekommen, was schief lief: Offenbar funktioniert das zur Verfügung Stellen einer Datei in PHP so, dass die Antwort vom Server quasi ein Datenstrom ist, der die Datei überträgt, meine echo-Ausgaben haben aber an diesem Datenstrom rumgepfuscht und die PDF-Datei zerstört. Nach Entfernen aller echo-Ausgaben kommt nun die PDF-Datei unversehrt an. Das führt nun leider dazu, dass ich keine Log-Ausgaben mehr sehen kann. Gibt es da irgendwelche Tricks, wie man trotzdem an Log-Ausgaben kommen kann?
 
@RenoV Danke für den Tipp! Ich hab das jetzt mal so versucht:


PHP:
function logf($variable) {
    $file = '/var/www/html/php.log';
    $current = file_get_contents($file);
    if(gettype($variable) == "object" || gettype($variable) == "array") {
        $stringified_variable = http_build_query($variable);
    } else {
        $stringified_variable = $variable;
    }
    $current .= $stringified_variable;
    $current .= "\n";
    file_put_contents($file, $current);
}

Klappt an sich auch, aber irgendwie funktioniert das nicht, wenn ich die Funktion innerhalb einer Klasse aufrufe, die in der Hauptdatei via require eingebunden wird. Habe aus Verzweiflung schon die Funktion als Methode in der Klasse nochmal definiert und dann mit $this->logf($variable) aufgerufen, aber in der Log-Datei kommt trotzdem nichts an...
 
Photon schrieb:
einer Klasse aufrufe, die in der Hauptdatei via require eingebunden wird
Stichwort Dependency Injection
Hint: entweder in der Methode übergeben oder im Constructor (und eine Property nutzen)
Alternativ gäbe es in PHP noch die Möglichkeit der Traits.

edit: und statt require schaue dir mal Autoload an

Kleiner Tip:
file_put_contents:flags
Spare dir das Einlesen des Files und appende einfach.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: floq0r
RenoV schrieb:
Kleiner Tip:
file_put_contents:flags
Spare dir das Einlesen des Files und appende einfach.
Ah ja, danke, das ist wirklich kürzer so!

RenoV schrieb:
Stichwort Dependency Injection
Hint: entweder in der Methode übergeben oder im Constructor (und eine Property nutzen)
Alternativ gäbe es in PHP noch die Möglichkeit der Traits.
Ich stehe gerade auf dem Schlauch...

Sieht bei mir so aus:

PHP:
// Hauptdatei:

function logf($variable) {
    $file = '/var/www/html/php.log';
    if(gettype($variable) == "object" || gettype($variable) == "array") {
        $stringified_variable = http_build_query($variable) . "\n";
    } else {
        $stringified_variable = $variable . "\n";
    }
    file_put_contents($file, $stringified_variable, FILE_APPEND);
}

require_once('myClass.php');

myClass::myMethod(logf);

// Datei myClass.php:

class myClass {
    public static function myMethod($logger) {
        $logger('blabla');
    }
}

Resultat ist, dass nichts in die Logdatei geschrieben wird. Wo bin ich falsch abgebogen? :)
 
Dein Beispiel sollte eigentlich nen Fatal Error werfen, weil du eine Function so nicht übergeben kannst. Das "logf" im function call wird als Konstante gewertet von PHP. (edit: ah, pre 8 ist das nur ne Warning)

(ich setze mal PHP 8 voraus)
Der einfachste Weg (deiner) wäre eher so korrekt:
PHP:
<?php

$logger = function(string $message): void{
    echo $message;
};

class foo{
    public function bar($logger): void{
        $logger('message in function');
    }
}

$obj = new foo();
$obj->bar($logger);

Der bessere Weg, wenn man eh schon mit Klassen arbeitet:

PHP:
<?php

class logger{
    public function log(string $message):void{
        echo $message;
    }
};

class foo{
    public function bar(logger $logger):void{
        $logger->log('message in function');
    }
}

$logger = new logger();
$obj = new foo();
$obj->bar($logger);

Oder, wenn es gleich komplett richtig sein soll:

PHP:
<?php

interface iLogger{
    public function log(string $message);
}

class FileLogger implements iLogger
{
    public function log(string $message): void
    {
        echo $message;
    }
}

class DBLogger implements iLogger
{
    public function log(string $message): void
    {
        echo 'writes "'.$message.'" in db';
    }
}

class foo
{
    public function bar(iLogger $logger): void
    {
        $logger->log('message in function');
    }
}


$obj = new foo();
$logger = new FileLogger();
$obj->bar($logger);
$logger = new DBLogger();
$obj->bar($logger);

Man könnte die Logger-Klassen noch static machen, aber du willst ja vllt noch Path und File definieren. Bei static müsstest du das jedes Mal neu mit übergeben (func log($msg, $file). So kannst du im Konstruktor der Logger-Klasse das einmalig festlegen.

edit: der Vollständigkeit halber noch der Weg über Traits

PHP:
<?php

trait loggerTrait
{
    function log(string $message): void
    {
        echo $message;
    }
}

class foo
{
    use loggerTrait;

    public function bar()
    {
        $this->log('message in function');
    }
}

$obj = new foo();
$obj->bar();
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Photon
Ja super, das funktioniert (also die erste Methode)!

Jetzt habe ich noch ein weiteres Problem, das wahrscheinlich nicht so einfach zu lösen ist... Das PHP-Skript macht im Moment Folgendes:

1. Nutzereingaben aus einem HTML-Formular auffangen.
2. Eine Text-Datei (LaTeX-Template) einlesen, die PHP-Blöcke enthält.
3. PHP drüberlaufen lassen und dabei die aufgefangenen Eingaben nutzen, um die PHP-Blöcke auszuwerten.
4. Die Text-Datei auf dem Dateisystem abspeichern.
5. Mittels exec() einen LaTeX-Compiler drüberlaufen lassen, der eine PDF-Datei generiert.
6. Die generierte Datei dem Nutzer ausliefern.

Punkte 2. bis 4. sehen dabei so aus:

PHP:
ob_start();
include($template_file);
file_put_contents($tex_f, ob_get_clean());

So weit ich es verstehe, wird durch den include()-Befehl die PHP-Datei um den Inhalt der LaTeX-Template erweitert.

Jetzt würde ich gerne mehrere PDF-Dateien generieren, die dann zusammengefügt werden und das Ergebnis dem Nutzer ausgeliefert wird. Aber sobald ich die erste Datei auf die Art erzeugt habe, ist ja in meiner PHP-Datei der Inhalt der LaTeX-Template enthalten und "stört" das Generieren der nächsten Datei (der Download ist wie in der ursprünglichen Frage beschrieben korrupt).

Frage: Kann ich das include() irgendwie rückgängig machen, sodass das Generieren der nächsten PDF-Datei wieder mit einer sauberen PHP-Datei vonstatten geht?
 
Ich bin mir grad nicht ganz sicher, was du da machst. Aber das $template_file enthält Text (und kein PHP-Code)?
 
Sorry, ist echt sehr verwirrend. :)

$template_file enthält größtenteils Text, aber auch einige wenige PHP-Code-Schnipsel, die dazu dienen, Nutzereingaben in diesen Text einzufügen.
 
Ja Wahnsinn, das scheint tatsächlich zu klappen, vielen Dank!

Wie man sich denken kann, ist das nicht mein Code, ich versuche bloß mit meinen bescheidenen bis hin zu nicht vorhandenen PHP-Kenntnissen das hier zu benutzen: https://mike42.me/blog/how-to-generate-professional-quality-pdf-files-from-php

Dann mal schauen, was da sonst alles schief läuft, jetzt wo Logging möglich ist, ist es gleich viel angenehmer!

edit: Ich glaube, das Leeren des Buffers hat auch schon das Problem gelöst, für dessen Debugging ich ursprünglich das Logging hernehmen wollte. :D
 
Ich tendiere eher zum Gegenteil.
Ich finde den PHP-Code im template und das dann per include einbinden eher nicht so prickelnd.
Ich würde im template nur den reinen Text lassen, in eine Variable einlesen und dort dann das ganze Replacement drüber laufen lassen.

Das Beispiel in dem Link ist auch von 2014 und als eher veraltet anzusehen (wenn ich die @ sehe... schüttel)
 
Verstehe, danke für die Erklärung!

Ich kann so etwas leider überhaupt nicht einschätzen, da ich nur sehr wenig Erfahrung mit PHP habe und auch allgemein ziemlich wenig Programmiererfahrung... Jetzt muss ich das Ding mit minimalem Zeitaufwand fertig kriegen, weil ich es in einer Woche schon werde benutzen müssen. Es geht um eine Hausarbeit, bei der eine Unterrichtsmethode in Mathe ausprobiert werden soll, in meinem Fall soll es ein Memory-Kartenspiel werden (das ich versuche via PHP und TeX generieren zu lassen) und ich bin mit dem Stoff bald so weit, dass es zum Einsatz kommen müsste. Deswegen hab ich leider nicht die Zeit, den Code ordentlich zu machen sondern schustere ihn irgendwie zusammen, damit er bloß funktioniert. In den Sommerferien kann man sich ja Zeit für Refactoring nehmen, wenn sonst nichts ansteht. Würde da sicher viel dabei lernen!
 
Zurück
Oben