如何从单独的PHP脚本执行PHP脚本而不会丢失第二个脚本的输出?

I am currently working on a project that uses HTML and PHP to allow a user to enter information into a web form in order to generate an Excel file. On submission, the form runs a PHP file, main.php:

<?php
    // output headers so that the file is downloaded rather than displayed
    $order = $_POST["order"];
    header('Content-Type: text/plain; charset=utf-8');
    header('Content-Disposition: attachment; filename="'.$_POST['order'].'.xls"');
    exec('php xl.php');
    sleep(2);
    readfile('xl_template.xls');
    error_reporting(E_ALL);
    ini_set("display_errors", 1);
?>

where "xl.php" is another PHP file and "xl_template" is a template for the Excel sheet I wish to modify. The purpose of main.php is to grab the modified template and download it to the user's computer, while xl.php actually modifies the Excel template and saves it to the server computer (using PHPExcel library):

<?php
    // this file will be called by main.php
    // after execution, there should be a newfile.xls
    // for the main.php to read from
    error_reporting(E_ALL);
    require('Classes/PHPExcel.php');

    // variable definitions
    $template = "xl_template.xls";

    $objPHPExcel = PHPExcel_IOFactory::load($template);
    $objWorksheet = $objPHPExcel->getActiveSheet();
    $objWorksheet->getCell('A2')->setValue('123400000000000000000001');
    $objWorksheet->getCell('B2')->setValue('it worked!');
    $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5');
    $objWriter->save($template);
?>

This works as expected when the xl.php file is called from terminal and then the form is filled out. However, when xl.php is called from main.php through the exec('php xl.php') method, the Excel file does not get updated, which I assume to mean that xl.php was not successfully executed.

In addition to exec(), I have tried system(),shell_exec, the backtick operator, and passthru(), with the same results. Also, I have tried Javascript and JQuery methods, calling the xl.php file with $.get('xl.php') and $.ajax({url: 'xl.php'}) with no luck.

Any insight into this problem would be greatly appreciated, as I am still new to using PHP. Thanks.

I would suggest making two separate functions and calling them in the order you need to to prevent the error from occurring. This is the most efficient way to do this.

If you really want to use a different page, you could could possibly use the header() function in php to redirect to the second php page, and include any variables you need to pass in the URL and retrieve them using $_GET().

Problem 1

You are using a pretty weird way of loading your second PHP file. While exec("php xl.php") will indeed execute xl.php, it will do it as if you were executing it from a command line. Your xl.php will not have access to any variables or other information from your outer script, and it can't write anything to the output which is sent to the user. Also, it is much harder to debug any errors occuring in the second script this way.

You should instead use require("xl.php"); which will run the xl.php file at this point as if its content were directly part of your outer script.

Problem 2

You are writing to a file on the server. This is something you should avoid doing unless absolutely necessary (e.g. for user uploads).

  • In your case, the write probably fails in the first place because you write it into the same directory your PHP file resides in. Usually, PHP doesn't have permission to write in there - for a good reason, because if your webserver (e.g. Apache) will only run PHP files in this directory, and PHP doesn't have the rights to write into it, then it is a lot harder for an attacker to put their own malicious PHP code in there and run it, or modify one of your scripts to add malicious code to it.
  • Assuming you got the permissions issue solved (which should be done by using a separate directory outside of the PHP script directory, possibly just the temporary directory - not by changing the permissions to allow writing into the script directory!), then you have another problem: Your filename is constant. Now assume two users download an Excel file at the same time... Everything will get messed up because two sessions will try writing/reading to/from the same file.
  • Assuming you fixed this by using a random temporary name (and then also delete the file again at the end), then there is still the issue that you are accessing the hard disk when it's not really needed. You could instead just directly stream the content of the Excel file from memory to the user's browser.

So, the best solution would be not using a file at all, but directly sending the data to the user. This can be done using the pseudo-filename php://output which basically sends all data written to it directly to the user.

Problem 3

Your content type header specifies text/plain and UTF-8 as charset, but you are sending an Excel file. Since an Excel file is not plain text (and probably not UTF-8 either), you should use the right MIME type instead, which is application/vnd.ms-excel.

Problem 4

You are turning error reporting on only after all the action already happened. This means that if an error occured, it will not be reported the way you expect, because it happens before the reporting is enabled.

Final example

Incorporating the fixes mentioned above, your code could look something like this:

main.php:

<?php

    error_reporting(E_ALL);
    ini_set("display_errors", 1);

    // output headers so that the file is downloaded rather than displayed
    $order = $_POST["order"];
    header('Content-Type: application/vnd.ms-excel');
    header('Content-Disposition: attachment; filename="'.$_POST['order'].'.xls"');

    require("xl.php");
?>

xl.php:

<?php
    // this file will be called by main.php
    require('Classes/PHPExcel.php');

    $objPHPExcel = PHPExcel_IOFactory::load($template);
    $objWorksheet = $objPHPExcel->getActiveSheet();
    $objWorksheet->getCell('A2')->setValue('123400000000000000000001');
    $objWorksheet->getCell('B2')->setValue('it worked!');
    $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5');
    $objWriter->save("php://output");
?>

Final thoughts

Now that you change the code like this, it would probably be best to think about whether this construct with the second PHP file is even needed like this, and if it is, whether you maybe want to wrap the code in a function and call it from the outer script so you can nicely pass parameters as well, because I assume you are not always going to write just staticly "It works!" in there.