Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

zorkwarrior's avatar

Download a .php file using Storage::download()

I'm rebuilding a site in Laravel to test out the framework. It has a frontend and handles requests from a legacy C# app.

The C# app downloads a number of files via GET requests. All of these serve up fine, except when I try to download a file with the .php extension (ie. file.php). The framework serves me up a very simple page instead of a download:

<body>File not found.</body>

Typically, if I request a file that doesn't exist, it serves an empty page (I know it should serve a error page instead, but I'm trying to keep this brief).

I run these requests through a controller that checks for the file's existence before download:

// Route
Route::controller(DownloadsController::class)->group(function () {
   ...
    Route::get('/downloads/{directory}/{filename}', 'downloadSubDirectory');
	...
});

// DownloadsController
    public function download($filename)
    {
        return Http::DownloadFile('cloudconnect/'.$filename);
    }
...
// In Http class
    public static function DownloadFile($filePath, $diskName = 'public')
    {
        $storage = Storage::disk($diskName);

        if($storage->exists($filePath))
            return $storage->download($filePath);

        return null;
    }

I can get this to work if I make a directory in /public/ and serve the file there, but that seems to be counter to everything the framework is trying to achieve.

I'm guessing there's some regex at the beginning of the request that matches any .php file and tries to process it before passing it to the Router.

EDIT: I know that downloading a .php file instead of processing it is not 'good'. The only reason I'm trying to do this is to support this legacy app that downloads a .php file (which is actually just a .xml file). I can't change the behavior of this legacy app. I know this is bad practice. The way it was handled before was open access to the directory listing, so people could download whichever file they wanted - which I recognize is also bad practice.

I was just wondering if there was some way I could serve this file explicitly? Or to phrase it differently, is the only way to serve a static .php file (not a blade template) by placing it in the /public/ directory?

0 likes
8 replies
martinbean's avatar

@zorkwarrior The Storage facade is a filesystem abstraction. You should be using it to interact with files and directories; not make HTTP requests to download.

You also won’t be able to download a .php file, because PHP files are processed by the server and the resultant HTML is output to the browser. If people could download .php files, then we’d all have the source code for Facebook, or the source for any other PHP-based website.

zorkwarrior's avatar

@martinbean I thought the Storage facade might allow me to download a .php as opposed to rendering it. Do you know what class handles the filtering of the request?

I've also updated my post with more context.

kokoshneta's avatar

This sounds like an XY problem – why do you want to download a PHP file? Downloading PHP files is generally a bad idea.

kokoshneta's avatar

@zorkwarrior Wow, so it’s really an XML file with a PHP file extension, which the app downloads? That sounds… dreadful.

Are you sure the app actually downloads the file, though? That implies a browser which reads the contents of the file and saves it into a file on the user’s (or in this case app’s) machine. It would make more sense for the app to read the contents of the file as an HTTP stream, rather than actually downloading it as a file.

In either case, it shouldn’t be difficult to do. You’ll have to set the appropriate headers, then use something like file_get_contents() to output the contents in the appropriate way, then close the connection.

zorkwarrior's avatar

Not sure if this is really a Laravel issue:

I noticed that I could download the file on my dev machine (upon running php artisan serve), but not in production (via nginx). So I guess somewhere along the way, there is some very functional difference between the way that php-cli runs the php builtin server vs how php-fpm serves files.

Unfortunately I'm not too fluent with the ways of nginx

kokoshneta's avatar

@zorkwarrior If you use something like file_get_contents and set the correct headers, the way the server or php-fpm/php-cli handles and serves PHP files doesn’t matter, because you’re not involving them at all. You’re simply reading the contents of a plain text file, as plain text, and then outputting those same contents as a text string and streaming them into a file on the user’s machine.

zorkwarrior's avatar

@kokoshneta Yeah the app does download the file. It uses the C# WebClient::DownloadData() method to do so. Because 'legacy', I can read but not update this source. Actually it uses this .xml/.php file to know what other files it needs to download.

The problem I'm running into, is that even though I can serve the information from that file, I seem to be getting a 404 from it. I've set the headers (via stream_context_create()) to match what I would get before (with open directory listings), but that doesn't seem to work. For that matter, my content type isn't getting set properly either so I'm definitely doing something wrong.

// DownloadsController.php
public function downloadFile()
{
        if(!file_exists($filePath))
            return null;

        // Create a stream
        $headers = "HTTP/1.1 200 OK\r\n" .
            "Vary: Accept-Encoding\r\n" .
            "Keep-Alive: timeout=5, max=100\r\n" .
            "Connection: Keep-Alive\r\n" .
            "Content-Length: " . filesize($filePath) . "\r\n" .
            "Content-Type: text/xml; charset=UTF-8\r\n";
            "Date: " . date('now') . "\r\n".
            "Server: "  . $_SERVER['SERVER_SOFTWARE'] . "\r\n";

        $opts = [
            'http' => [
                'method' => "GET",
                'header' => $headers,
            ]
        ];

        $context = stream_context_create($opts);

        return file_get_contents($filePath, false, $context);
//        $fileContents = file_get_contents($filePath, false, $context);
//       $escapedContents = htmlentities($fileContents);

//        return $escapedContents;
}

EDIT: Actually I don't get anything but a 'File not found.' in my browser unless I set a header in nginx. The one that gives me something is:

server {
		...
        location = /downloads/file.php {
                add_header Content-Type text/xml;
                add_header Content-disposition "attachment; filename=$1";
        }
}

Please or to participate in this conversation.