Slim PHP - How to Serve Static Files in Slim PHP
Recently I deployed a Slim PHP application for the first time and there were some slight learning curves that I had to undergo with Slim that you normally don’t have to think about with other full-featured frameworks like Laravel or Symfony.
Slim is designed to be exactly what the name says - slim. And with Slim’s minimalism, there are a good amount of features left out. That’s mostly because Slim is designed to be a lightweight microframework aimed at building simple APIs, however, Slim doesn’t try to limit your options, which I like.
TLDR
Skip to the solution.
Requirements
- Slim v4
- PHP 7.2 or newer
Our Application
This is the structure of our simple Slim application that renders an index.html
page. The index.html
page is also including some local JavaScript and CSS located in public/
that we want to load with our page.
1
2
3
4
5
6
7
8
9
10
├── composer.json
├── composer.lock
├── index.html
├── public
│ ├── app.css
│ ├── app.js
│ └── index.php
└── vendor
├── autoload.php
├── ...
Our Slim application is being loaded from public/index.php
.
public/index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
/**
* GET / - Load the Home Page
*/
$app->get('/', function (Request $request, Response $response, array $args) {
$response->getBody()->write(file_get_contents(__DIR__ . '/../index.html'));
return $response;
});
$app->run();
And our home page is index.html
.
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>Slim Static Assets</title>
<link rel="stylesheet" type="text/css" href="/app.css">
</head>
<body>
<div id="app">
<!-- Our app.js file will render content here that our app.css will style. -->
</div>
<script src="/app.js"></script>
</body>
</html>
The Problem
Our public/app.js
script will load some HTML content when the page is loaded which contains a CSS class that our public/app.css
will style. This is what we’ll use to know that our static assets are being loaded correctly.
public/app.js
1
document.getElementById("app").innerHTML = '<div id="hooray">Static files are being loaded correctly!</div>';
public/app.css
1
2
3
#hooray {
color: green;
}
We boot up the local development server with:
1
$ php -S localhost:8080 -t public public/index.php
When we start the local development server at localhost:8080
we should expect to see Static files are being loaded correctly!
in green colored text, but instead we can see that no content is being rendered and the console is reporting back 500 Internal Server Error
for the assets we are trying to load, despite the paths being correct.
What’s happening here is that our HTML page is trying to request the files through Slim, but our application doesn’t contain any paths to GET /app.js
or GET /app.css
, only GET /
, which loads the home page.
At first glance, the solution seems simple: we’ll just add a path for each asset file in our Slim application like so.
public/index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
/**
* GET /app.js - Load the application's JavaScript.
*/
$app->get('/app.js', function (Request $request, Response $response, array $args) {
$response->getBody()->write(file_get_contents(__DIR__ . '/app.js'));
return $response;
});
/**
* GET /app.css - Load the application's CSS.
*/
$app->get('/app.css', function (Request $request, Response $response, array $args) {
$response->getBody()->write(file_get_contents(__DIR__ . '/app.css'));
return $response;
});
...
However, we still end up with issues. The JavaScript appears to be executing correctly, but the CSS still isn’t working. When we open the console, we can see that the files are being found, but the Content-Type
header for both files is text/html
, which explains why our CSS isn’t being recognized based on the console warning.
One solution is to add the Content-Type header for each appropriate endpoint that is serving your static files.
public/index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
/**
* GET /app.js - Load the application's JavaScript.
*/
$app->get('/app.js', function (Request $request, Response $response, array $args) {
$newResponse = $response->withHeader('Content-Type', 'application/javascript; charset=UTF-8');
$newResponse->getBody()->write(file_get_contents(__DIR__ . '/app.js'));
return $newResponse;
});
/**
* GET /app.css - Load the application's CSS.
*/
$app->get('/app.css', function (Request $request, Response $response, array $args) {
$newResponse = $response->withHeader('Content-Type', 'text/css; charset=UTF-8');
$newResponse->getBody()->write(file_get_contents(__DIR__ . '/app.css'));
return $newResponse;
});
...
Now when we refresh the page, we can see that the JavaScript and CSS are being loaded correctly.
The Solution
Even though we’ve got the JavaScript and CSS loading now, there’s still two things to consider:
- What if I need to load several static files? Am I really going to create an endpoint for every single static file?
- That
favicon.ico
is still returning a500
when a404
is probably more appropriate. How can we handle non-existent files?
We can handle both of these considerations in the following solution.
The final result looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* GET /{file} - Load a static asset.
*
* THIS MUST BE PLACED AT THE VERY BOTTOM OF YOUR SLIM APPLICATION, JUST BEFORE
* $app->run()!!!
*/
$app->get('/{file}', function (Request $request, Response $response, $args) {
$filePath = __DIR__ . '/' . $args['file'];
if (!file_exists($filePath)) {
return $response->withStatus(404, 'File Not Found');
}
switch (pathinfo($filePath, PATHINFO_EXTENSION)) {
case 'css':
$mimeType = 'text/css';
break;
case 'js':
$mimeType = 'application/javascript';
break;
// Add more supported mime types per file extension as you need here
default:
$mimeType = 'text/html';
}
$newResponse = $response->withHeader('Content-Type', $mimeType . '; charset=UTF-8');
$newResponse->getBody()->write(file_get_contents($filePath));
return $newResponse;
});
You should absolutely put this at the end of your Slim application, just before $app->run()
. The reason is because when Slim receives a request to a path, it executes from the top down until it finds a matching path. The first path that gets matched gets returned. If you put an endpoint underneath the solution given above, it may never reach that endpoint.
After we have included the new endpoint, we refresh the page and see that all errors are completely gone and all assets are being loaded correctly.
Conclusion
Slim may not come with a way to serve static assets out-of-the-box, however, Slim still gives you the freedom to incoporate what you need, however you need it, when you need it. It is a great choice for a micro-framework when you need something simple that gets the job done.