phpregex.htaccessweb-hosting

how can I write .htaccess rules to simulate my local server that sets the document root to a directory named 'public'


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?


Solution

  • The .htaccess rule probably isn’t working how you expect. (I assume here because I’ve run into the same problem repeatedly.)

    Understanding .htaccess instructions

    RewriteCond %{REQUEST_URI} !^/public/
    RewriteRule ^$ /public/$1
    

    What is happening

    Solution

    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.

    *Note on 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).

    Resources