skip to Main Content

Background

I am writing a web project with PHP without framework. I used various packages to write in MVC pattern. For the routing part (I used League/Route), I have to set some rewriting rules on Apache server for the routes to work.

The company’s server is set up in a way that all application codes sit outside of the document root, and alias is used to internally redirect to the application. This leads to a lot of trouble in terms of relative paths, routes and URL rewriting.

I’m going to break down the problems into steps, and try to understand one at a time. If you want to see the overall issue I’m having, please refer to this question

PART 1

The router compares $_SERVER["REQUEST_URI"] value to routes defined to figure out which action it maps to. Here are routes:

$router->map("GET", "/", Hello1Controller::class);
$router->map("GET", "/hello2", Hello2Controller::class);

Here is the navigation bar, just something to demonstrate the routing:

<li><a href="/">Hello 1</a></li>
<li><a href="/hello2">Hello 2</a></li>

Here are the document root and rewrite rule in httpd.conf:

DocumentRoot "/usr/local/var/www/app/public"
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /index.php$1 [L]

I have a template.twig sitting outside of the document root (not in public directory), and within template.twig I try to reference a css file:

<link href="css/style.css" type="text/css" rel="stylesheet"/>

This css file is in public/css/style.css, so it’s inside the document root.

Now I try to access https://localhost, and the URL is rewritten to /index.php, triggering the front controller. The front controller then maps the REQUEST_URI (/) to Hello1Controller, which renders the template.twig. If we click "hello 2" on the navbar, it successfully navigates to that page.

ISSUE: relative path in resource reference not working:
A GET request is issued for each resource referenced in the template. RewriteCond %{REQUEST_FILENAME} !-f is going to test whether the referenced resource is a file or not. But %{REQUEST_FILENAME} turns out to be just the relative path with a / in front (made it relative to root I guess), not the full path after it’s figured out. And this will obviously not be a file, resulting in the rewrite rule being applied to a regular file reference and sent to the front controller. You can see this from the log:

[rewrite:trace2] init rewrite engine with requested uri /css/style.css, referer: http://localhost/
[rewrite:trace3] applying pattern '^(.*)$' to uri '/css/style.css', referer: http://localhost/
[rewrite:trace4] RewriteCond: input='/css/style.css' pattern='!-f' => matched, referer: http://localhost/
[rewrite:trace2] rewrite '/css/style.css' -> '/index.php/css/style.css', referer: http://localhost/
[rewrite:trace2] local path result: /index.php/css/style.css, referer: http://localhost/
[rewrite:trace2] prefixed with document_root to /usr/local/var/www/app/public/index.php/css/style.css, referer: http://localhost/

And notice the requested uri is "/css/style.css", but in code I used relative path css/style.css. This makes me think relative path is relative to the current "position" in url. Not sure whether it’s before or after rewrite, but in this case result is the same–relative to either the root (because there is nothing in the path part of the uri), or index.php (which is in the same directory as root). Both resulting in the "/css/style.css" we see, instead of /usr/local/var/www/app/public/css/style.css.

Now that if this rewrite rule is being applied in a per-directory context, then it works: %{REQUEST_FILENAME} is now the relative path appended with the directory, making it a full path leading to the file.

DocumentRoot "/usr/local/var/www/app/public"
<Directory "/usr/local/var/www/app/public">
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ /index.php [L]
</Directory>

(RewriteRule had to be tweaked a bit in order for hello2 to work. This shouldn’t affect the relative path)

[rewrite:trace3] [perdir /usr/local/var/www/app/public/] strip per-dir prefix: /usr/local/var/www/app/public/css/style.css -> css/style.css, referer: http://localhost/
[rewrite:trace3] [perdir /usr/local/var/www/app/public/] applying pattern '^(.*)$' to uri 'css/style.css', referer: http://localhost/
[rewrite:trace4] [perdir /usr/local/var/www/app/public/] RewriteCond: input='/usr/local/var/www/app/public/css/style.css' pattern='!-f' => not-matched, referer: http://localhost/
[rewrite:trace1] [perdir /usr/local/var/www/app/public/] pass through /usr/local/var/www/app/public/css/style.css, referer: http://localhost/

Question: Is this just the default behavior of relative path in rewriting rules? per-directory or .htaccess is always required for it to work?

Also here is the observation I made about relative path. Please correct me if I’m wrong:

The relative paths in the client-side code (in html, css, or the twig templates, in which the included resources are not resolved until passed to the client side):

  • If the file referencing a resource with relative path is inside the document root, then relative path works intuitively: it’s relative to the current file.
  • If the file referencing a resource with relative path is outside of the document root (e.g. in a template file being rendered and sent to the client side), then the path is not relative to the current file. It is relative to the current url. That is, a ".." will go back one level in the url. This is preventing me to use relative path in those files.

PART 2

Now I change the server setting so that the document root is no longer inside the application, but instead app sits entirely inside document root. I soft linked app/ into www/

DocumentRoot "/usr/local/var/www"
RewriteEngine On
RewriteRule ^/?$ /app/public/index.php [L]
<Directory "/usr/local/var/www/app/public">
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ /index.php [L]
</Directory>

In order for https://localhost to internally redirect to my application, I added another rewrite rule.
Now the resource referencing is not working again:

[rewrite:trace2] init rewrite engine with requested uri /css/style.css, referer: http://localhost/
[rewrite:trace3] applying pattern '^/?$' to uri '/css/style.css', referer: http://localhost/
[rewrite:trace1] pass through /css/style.css, referer: http://localhost/
[core:trace3] request authorized without authentication by access_checker_ex hook: /css/style.css, referer: http://localhost/
[core:info] AH00128: File does not exist: /usr/local/var/www/css/style.css, referer: http://localhost/

The per-directory definition is not even processed. I tried different variations of the rewrite rules, including adding and removing [L], changing the order of two rewrite rules hoping the per-directory one gets executed first, wrapping the first rewrite rule with its own directive, etc… None of these worked.
The "hello 2" tab on the navbar isn’t working as well due to the same reason––per-directory rewrite rules are not processed at all.

PART 3

Next step is to move the application entirely outside of the document root, and use alias to internally redirect to the app.

2

Answers


  1. Chosen as BEST ANSWER

    Well I found a solution for Part 2:

        DocumentRoot "/usr/local/var/www"
        <Directory "/usr/local/var/www">
            RewriteEngine On
            RewriteRule ^$ app/public/ [L]
            RewriteRule (.*) app/public/$1 [L]
        </Directory>
    
        <Directory "/usr/local/var/www/app/public">
            RewriteEngine On
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteCond %{REQUEST_FILENAME} !-d
            RewriteRule ^ index.php [L]
        </Directory>
    

    We need to have a per-directory rewrite rule at the root level to append the app's path to the docroot, before it's evaluated by the per-directory directives at app level.

    This isn't anything official that I read, but rather something I see from experiment:
    The path part of uri (after any previous rewrite rules have been applied) has to match the one specified in the "Directory" directive in order for it to execute. So at the beginning we are at the document root, so the root level per-directory directive executes. It appends the app/public part before the uri, so now the uri path contains the whole path leading to the app's public directory. Thus, the app's per-directory directives can be executed.

    Part 3 can be resolved in similar fashion, with the addition of alias. This answer provides an example for it.


    Note: Part 2 implementation has no practical meaning and should be avoided in real production. It is just an intermediate step I took to solve the ultimate problem. Only your public folder (the one containing static content), if at all, should live inside the document root.


  2. Welcome to Stackoverflow!

    Looking at what you have there, it doesn’t appear like you need to do what you have written in your answer.

    if your project sits in: /usr/local/var/www/app
    and your document root is: /usr/local/var/www/app/public

    It looks like you are a little confused between your projects root folder and the HTTP server DocumentRoot.

    The HTTP Server DocumentRoot, is the folder that apache should treat at the root. i.e. when you go to http://myapp.com/index.php, it will look in the DocumentRoot for index.php.

    Assuming you have configured your site as a VirtualHost, as most do, your vhost config would look something like this:

    <VirtualHost *:80>
        ServerAdmin [email protected]
        DocumentRoot "/usr/local/var/www/app/public"
    
        <Directory /usr/local/var/www/app>
            AllowOverride all
        </Directory>
        ServerName myapp.local
    
        ErrorLog "/var/log/myapp.local-error_log"
        CustomLog "/var/log/myapp.local-access_log" common
    </VirtualHost>
    

    Then in your .htaccess file that sits in the DocumentRoot: /usr/local/var/www/app/public you would have:

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule .* /index.php$0 [PT]
    

    You should also use a preceding slash in your HTML to refer to the css file, as the / will ensure that the path is relative to your DocumentRoot, which is where your css folder is.

    The key thing here is that the only files that need to be in the DocumentRoot, are your .htaccess file, your index.php file and your assets. Your application will then refer to the code in the “app” folder by including files using the previous directory operator ../ in most cases/frameworks this would be something like include('../bootstrap.php');.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search