I'm trying to host an app' on a shared server, where I have no control over the server configuration and with that, the DOCUMENT_ROOT
is the PROJECT_ROOT
so all files are accessible through the browser. Therefore, I'm trying to replicate what I have on my local machine, where I have a folder named public
as the DOCUMENT_ROOT
which is a sub-folder of the PROJECT_ROOT
.
I start my local server with the command:
php -S localhost:8888 -t public
which sets the public
folder as the DOCUMENT_ROOT
. The directory structure is as follows:
project_root
|
|---controllers
| |
| |----notes
| | |
| | |---create.php
| | |---index.php
| | |---show.php
| |
| |
| |---about.php
| |---contact.php
| |---index.php
|
|
|---Core
| |
| |---Database.php
| |---functions.php
| |---Response.php
| |---router.php
| |---Validator
|
|
|---public - document root
| |
| |----index.php
|
|
|---views
| |
| |----notes
| | |---create.php
| | |---index.php
| | |---show.php
| |
| |
| |----partials
| | |---banner.php
| | |---footer.php
| | |---header.php
| | |---nav.php
| |
| |
| |---403.php
| |---404.php
| |---about.php
| |---contact.php
| |---index.php
|
|
|---.htaccess
|---config.php
|---routes.php
The entry point to the app' is project_root/public/index.php
const BASE_PATH = __DIR__.'/../';
// var_dump(BASE_PATH); // "C:\xampp\htdocs\project_root\public/../"
require BASE_PATH.'Core/functions.php';
// php calls this function automatically when a class is required
spl_autoload_register(function ($class) {
require base_path("Core/" . $class . '.php');
});
// appends BASE_PATH to the given path
require base_path('Core/router.php');
I have some helper functions in Core/functions.php
, including
function base_path($path)
{
// append BASE_PATH so we don't have to remember what the path is
return BASE_PATH . $path; // const BASE_PATH = __DIR__.'/../'
}
and I have a very basic router, which resides in project_root/Core/router.php
:
function routeToController($uri, $routes) {
if (array_key_exists($uri, $routes)) {
require base_path($routes[$uri]);
} else {
abort();
}
}
function abort($code = 404) {
http_response_code($code);
require base_path("views/{$code}.php");
die();
}
$routes = require base_path('routes.php');
$uri = parse_url($_SERVER['REQUEST_URI'])['path'];
routeToController($uri, $routes);
with routes set in project_root/routes.php
return [
'/' => 'controllers/index.php',
'/about' => 'controllers/about.php',
'/notes' => 'controllers/notes/index.php',
'/note' => 'controllers/notes/show.php',
'/notes/create' => 'controllers/notes/create.php',
'/contact' => 'controllers/contact.php',
];
which maps requests to the appropriate controller. (The controllers only contain the view()
helper function that takes the path
and attributes to set a heading
variable.)
When I start the local development server with the command php -S localhost:8888 -t public
everything works as expected and I can click links to home
about
notes
& contact
to display the relevant document.
On the shared server, I have tried various .htaccess rules and the best I've got so far is:
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^$ /public/$1
This successfully runs project_root/public/index.php
which in turn loads the controller project_root/controllers/index.php
that calls project_root/views/index.php
:
// project_root/views/index.php
// import partials:
<?php require('partials/head.php') ?>
<?php require('partials/nav.php') ?>
<?php require('partials/banner.php') ?>
<main>
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<p>Hello. Welcome to the home page.</p>
</div>
</main>
<?php require('partials/footer.php') ?>
All the partials
are successfully loaded, but, when I click on a link to any other page (about
, notes
or contact
) I get a 404 page.
When I look in the browser's network monitor and click the about
link I get the following method, filename, and status:
GET
filename /about
Status 302 Found
When the homepage loads, or I click on the home
link I get:
GET
filename /
Status 200 OK
What do I need to do to get the other pages to load? I've spent hours looking at documents on .htaccess including the Apache docs, but, it all sees so far over my head that I really don't know what I'm doing. Give me a year or so and maybe....! But I really am stuck for now and any help would be very much appreciated.
Thanks in advance if you can help me out. This seems to be a common issue that people run into, but I have yet to find a successful solution. Is it even possible to achieve what I want using .htaccess rules?
The .htaccess rule probably isn’t working how you expect. (I assume here because I’ve run into the same problem repeatedly.)
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^$ /public/$1
%{REQUEST_URI}
refers to the URL after the domain and before the query string. In https://example.com/foo?bar=baz
, it would return /foo
. None of your links will return a URI that starts with /public (^/public/
), so Apache’s pattern negation key (!
) will match for all of those links. So far so good!
^$
is a regular expression meaning "has a string start followed immediately by a string end"—an empty string. The RewriteRule uses it as a second pattern to match. The string will be whatever was referenced in the previous RewriteCond (the URI, in this case). Of course this pattern does not describe /about
. It won’t even describe https://example.com
, because %{REQUEST_URI}
will resolve to /
.
/public/$1
tells Apache that the real URI it should be working with is whatever the rule (not the condition!) matched, prepended by /public/
. (And given the ^$
pattern, $1
will only ever return an empty string.) Apache now thinks that when someone requests /about/
, that what they really mean is /public/about/
.
✅ /index.php
:
Apache happily rewrites their request to /public/index.php
, then retries the request on itself* (looking for /public/index.php
), which matches the ^/public/
pattern, which is negated, so the RewriteRule is skipped. Apache tries to run the file (which does exist, so no problems).
✅ /
:
Apache happily rewrites their request to /public/
, then retries the request on itself (looking for /public/
), which matches the ^/public/
pattern, which is negated, so the RewriteRule is skipped. Apache recognizes that /public/
is a directory reference not a file reference, and guesses that the directory has an index.php
you want to run. Apache tries to run the file (which does exist, so no problems).
❌ /about
:
Apache happily rewrites their request to /public/about
, then retries the request on itself (looking for /public/about
), which matches the ^/public/
pattern, which is negated, so the RewriteRule is skipped. Apache tries to run the file /public/about
, which does not exist!
Laravel and other single-entry frameworks use a slightly different matching pattern for the RewriteRule: ^
("has a string start"), which of course describes any string.
Then instead of including a RegEx variable, they point to their single entry-point file, and avoid using the R
flag so the file will be able to see the whole URI. They often follow it up with the flag L
or END
*, telling Apache to skip the rest of the .htaccess file. So if your final destination is /public/index.php
, the full line would become
RewriteRule ^ /public/index.php [L,QSA]
(If that doesn’t work, try ^(.+)$
, meaning "a string start, followed by anything one or more times, up through the last string end, and capture the anything in $1
".)
Note however that the URI seen by the PHP file will include /public/
. For this reason, you may want to put your own index.php
in the project root (which does nothing but include the "real" index.php), then point Apache to /index.php
instead of /public/index.php
.
L
vs. END
Both flags tell Apache "after this, don’t read the rest of the file and just go".
When Apache "goes," it normally applies itself to its destination, as though its destination were its own full request. This is the type of "go" meant by L
. In practice, it often means Apache will read the same .htaccess file more than once. It’s astoundingly powerful (particularly when you have nested .htaccess files), but counterintuitive.
The difference with END
is that Apache will skip not just the rest of that file, but all other .htaccess files it might otherwise have encountered. It’s intuitive but inflexible.
If you know PHP, it’s like the difference between "return" (L
) and "exit" (END
).