Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.x - Base path handling #2512

Closed
akrabat opened this issue Oct 2, 2018 · 29 comments
Closed

4.x - Base path handling #2512

akrabat opened this issue Oct 2, 2018 · 29 comments

Comments

@akrabat
Copy link
Member

akrabat commented Oct 2, 2018

We need to have base path handling working consistently without confusion in Slim 4.

See also: #1964

@l0gicgate
Copy link
Member

This should be moved to Slim-Psr7 repo along with #1964

@akrabat
Copy link
Member Author

akrabat commented Nov 25, 2018

This essentially means we should support running Slim in a subdirectory.

i.e. Slim should work for a URL like www.example.com/myapp/hello where the route in Slim is $app->get('/hello', HelloAction::class');

I would like this to be PSR-7 implementation independent.

@tuupola
Copy link
Contributor

tuupola commented Nov 26, 2018

Slim already works when running from a subdirectory. I do it all the time. Just need to set the RewriteBase in .htaccess file. For example for the above example it would be:

RewriteBase /myapp/

@l0gicgate
Copy link
Member

@tuupola wouldn't it be better to make it server environment independent somehow? We could maybe add a setting to App which appends a path prefix to all the routes when they're mapped maybe?

@tuupola
Copy link
Contributor

tuupola commented Nov 27, 2018

@l0gicgate I don't have any strong feelings about that.

However one cannot get rid of RewriteBase. It is not about prefixing Slim urls. It is about telling Apache where to find the index.php file.

Lets assume the document root is /var/www/public and Slim app is in subfolder foo.

/var/www/public
/var/www/public/foo
/var/www/public/foo/.htaccess
/var/www/public/foo/index.php
$app = new \Slim\App;
$app->get("/hello", function ($request, $response, $args) {
    print "Hello!";
});
$app->run();
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

Without RewriteBase when request is made to https://example.com/foo/hello Apache tries to access /var/www/public/index.php which is wrong. You will get a 404 response from Apache with something like:

The requested URL /var/www/public/index.php was not found on this server.

With RewriteBase when request is made to https://example.com/foo/hello Apache accesses /var/www/public/foo/index.php which is correct and you will get a response from Slim.

RewriteEngine On
RewriteBase /foo/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

Since RewriteBase is anyway needed I am not sure if another setting is needed? Maybe for Nginx and other servers?

@l0gicgate
Copy link
Member

I don't believe nginx has this problem since when you setup fastcgi you have to set the base path like so:

server {
    # FastCGI Parameters
    location ~ \.php$ {
        fastcgi_pass    unix:/var/run/php-fpm.sock;
        fastcgi_param   SCRIPT_FILENAME /var/www/html/foo$fastcgi_script_name;
        include fastcgi_params;
    }
}

@akrabat better documentation perhaps? This cannot be fixed at the Slim level from my understanding.

@akrabat
Copy link
Member Author

akrabat commented Feb 18, 2019

@akrabat better documentation perhaps? This cannot be fixed at the Slim level from my understanding.

I'm more than happy to have a definitive documentation page on how to run Slim in a sub-directory.

@tuupola
Copy link
Contributor

tuupola commented Feb 18, 2019

For Apache all you need to do is to add RewriteBase directive to the default Slim config. Works with both Slim 2 and Slim 3. I have been running Slim like this since the beginning.

Example of an .htaccess from one of my live projects. Slim is installed to folder named foo.

RewriteEngine On

RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

RewriteBase /foo/

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

The .htaccess must in the same folder where Slim's main entry point is. In the example below /var/www/public is Apache main document root. Slim app files are simply copied to foo folder under the document root. Slim app is accessible at example.com/foo/ url.

/var/www/public
/var/www/public/foo
/var/www/public/foo/.htaccess
/var/www/public/foo/index.php
/var/www/public/foo/src
/var/www/public/foo/vendor

Since it is bad habit to have vendor and src publicly accessible and maybe you use deployer for deploying the app, directory structure might look like this:

/var/www/public
/var/www/deployer/foo
/var/www/deployer/foo/public/.htaccess
/var/www/deployer/foo/public/index.php
/var/www/deployer/foo/src
/var/www/deployer/foo/vendor

With above folder structure /foo/public must also be aliased as /foo in Apache config.

Alias /foo/ /var/www/deployer/foo/public/

Now source code is outside document root but Slim app is still accessible at example.com/foo/ url.

@odan
Copy link

odan commented Feb 22, 2019

@tuupola Maybe RewriteBase is a little bit to "unflexible", because it's a hardly coded path and may change from development machine to development machine and from server (test, staging, prod) to server.

I think Slim 4 should handle this issue, like Symfony and other Frameworks can handle it.

In Slim 3 I do a little "hack" by...

  1. changing the SCRIPT_NAME in the $_SERVER super global variable:
$container['environment'] = function () {
    $scriptName = $_SERVER['SCRIPT_NAME'];
    $_SERVER['SCRIPT_NAME'] = dirname(dirname($scriptName)) . '/' . basename($scriptName);
    return new Slim\Http\Environment($_SERVER);
};
  1. ... and then I add a second .htaccess file above the public/ directory. The second .htaccess file only makes an internal redirect to the public/directory, but only if Apache DocumentRoot does not point to the public/directory (e.g. in my development environment).
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]

I hope that in Slim 4 this "hack" is no longer needed. :-)

@l0gicgate
Copy link
Member

@odan can you point me where the logic is from Symfony to circumvent this issue? Because I'm looking at Zend Expressive and they explicitly document that a server side configuration is required:
https://docs.zendframework.com/zend-expressive/v3/cookbook/using-a-base-path/

I'd like to fix this for 4.x but I don't think it's possible.

@odan
Copy link

odan commented Apr 24, 2019

@l0gicgate

It looks like I confused it with a feature in CakePHP.

Update: Here is my working solution using $app->setBasePath($basePath);

The basic directory structure:

  • public/ Web server files, the DocumentRoot
    • .htaccess Apache redirect rules for the front controller
    • index.php The front controller
  • .htaccess Internal redirect to the public/ directory

For Apache we have to "redirect" the web traffic to the front controller in public/index.php.

Create a file: public/.htaccess with this content:

# Redirect to front controller
RewriteEngine On
# RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

We also need a rule to "redirect" the sub-directories to the front-controller in public/index.php.

Create a .htaccess file above the public/ directory with this content:

RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]

Set the base path to run the app in a subdirectory. This path is used in urlFor().

Example:

<?php

use Slim\Factory\AppFactory;
use Selective\BasePath\BasePathDetector;

$app = AppFactory::create();

// composer require selective/basepath

// Set the base path to run the app in a subdirectory.
// This path is used in urlFor().
$basePath = (new BasePathDetector($_SERVER))->getBasePath();

$app->setBasePath($basePath);

// Add middleware
// ...

$app->run();

@adriansuter
Copy link
Contributor

I just installed Slim 4.0.0-alpha, configured an alias called slim4 to my virtual host localhost (Apache 2.4.34) pointing towards a subdirectory public of the installation and added the following .htaccess-file to that subdirectory (no RewriteBase needed)

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /slim4/index.php [QSA,L]

In the same subdirectory I then added a very simple index.php-file

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

include_once(__DIR__ . '/../vendor/autoload.php');

define('ROUTE_NAME_ANSWER', 'answer');

$app = AppFactory::create();
$app->setBasePath('/slim4');

$app->get('/', function (Request $request, Response $response, $args) use ($app) {
    $routeParser = $app->getRouteCollector()->getRouteParser();
    $response->getBody()->write('<a href="' . $routeParser->urlFor(ROUTE_NAME_ANSWER) . '">What is the answer?</a>');
    return $response;
});

$app->get('/answer', function (Request $request, Response $response, $args) {
    $response->getBody()->write('42');
    return $response;
})->setName(ROUTE_NAME_ANSWER);

$app->run();

Requesting http://localhost/slim4/ worked as expected. The link href was correctly set (so the route parser urlFor did prefix the base path) and http://localhost/slim4/answer worked also like a charm.

Of course if would be great if the base path could either be detected automatically, or if it could be set via settings.

One possibility would be to use Apache environment variables (if allowed by hosting provider):

  1. Add the following line to .htaccess
SetEnv SLIM_BASE_PATH /slim4
  1. Update the following line in index.php
$app->setBasePath($_SERVER['SLIM_BASE_PATH']);

@l0gicgate
Copy link
Member

@adriansuter that's a good solution. I suppose we should add that to the docs? Can we consider this issue resolved as well?

@adriansuter
Copy link
Contributor

@l0gicgate Maybe in the docs we should use filter_input(INPUT_SERVER, 'SLIM_BASE_PATH', FILTER_SANITIZE_STRING) instead of just $_SERVER['SLIM_BASE_PATH']. Would you add this to the docs? English is not my native language :-)

I haven't checked all possible use cases of the base path. Other than that, in my opinion the concept is pretty consistent and without confusion, as requested by @akrabat

@moritz-h
Copy link
Contributor

I just started looking at v4 alpha and directly saw this issue just after copying the hello world code.

I wondered why Slim 3 works out the box and found some base-path-detection code here:
https://github.com/slimphp/Slim/blob/3.12.1/Slim/Http/Uri.php#L201-L215

I would guess this was removed because of the PSR7 changes.
But couldn't some automatic base path detection not just be added in the AppFactory?

@l0gicgate
Copy link
Member

@moritz-h we could possibly include some type of middleware which appends the user defined base path to the request's uri's path.

We could do something like https://github.com/Lansoweb/basepath does.

@silentworks
Copy link
Member

@l0gicgate that's a good idea.

@moritz-h
Copy link
Contributor

@l0gicgate I think you mean something different. You talk about handling the user defined base path. What I meant is, that I do not want to define a base bath and Slim should auto-detect it (like Slim v3 does, when I run it in a subdirectory).

Currently I cloud either define $app->setBasePath(...), then the base path will be added to all route definitions. Or I cloud use something like Lansoweb/basepath which removes the base path from the request. But both ways work only with user defined base path.

What I was suggesting above is that perhaps in AppFactory the auto-detection is done, so there there is no need for calling $app->setBasePath().
But I could also do something like using the autodetection code from Slim3 in something like the Lansoweb/basepath Middleware. That should also work. I would then suggest to add this a example middleware to slim documentation.

@l0gicgate
Copy link
Member

@moritz-h feel free to raise a PR with base path detection logic in AppFactory. As I said before, it's not possible to be done without assumptions and for the record it does not work in Slim 3 either, hence why this is a longstanding issue.

@moritz-h
Copy link
Contributor

@l0gicgate I read #2107 only after my last post here. Before I only read Slim 3 sources and thought it is a matter of copy and paste and adjust this part of code. Now I see what the problem is with this.

@esetnik
Copy link

esetnik commented Aug 7, 2019

I implemented a BasePathMiddleware that seems to work.

<?php

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Psr\Http\Message\ResponseInterface;

class BasePathMiddleware
{
  private $basePath;

  public function __construct(string $basePath)
  {
    $this->basePath = $basePath;
  }

  public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
  {
    $uri = $request->getUri();
    $path = $uri->getPath();
    $path = '/'.ltrim($path, $this->basePath);
    $request = $request->withUri($uri->withPath($path));
    return $handler->handle($request);
  }
}

Use it like:

$app->addRoutingMiddleware();

$basePathMiddleware = new BasePathMiddleware('/api');
$app->add($basePathMiddleware);

@esetnik
Copy link

esetnik commented Aug 7, 2019

I left a note about this in #2770 (comment) because it tripped me up during migration from v3 to v4. I didn't see this open issue at the time.

@l0gicgate l0gicgate changed the title Base path handling in Slim 4 4.x - Base path handling Aug 7, 2019
@dominikzogg
Copy link

any idea, why PSR7 implementations are not using: $_SERVER['PATH_INFO'] ?

@l0gicgate
Copy link
Member

l0gicgate commented Aug 9, 2019

@dominikzogg typically when you're on localhost that variable does not get set. It's not reliable.

@phpbg
Copy link

phpbg commented Aug 30, 2019

$app->setBasePath() did the trick for me, but I couldn't find it in the doc.

IMO it should me mentionned on this page: http://www.slimframework.com/docs/v4/start/web-servers.html

@piotr-cz
Copy link
Contributor

piotr-cz commented Sep 23, 2019

  • When using $app->setBasePath('/public') it breaks RouteParser::relativeUrlFor output:

    echo $routerParser->urlFor('about');
    /public/about
    
    echo $routerParser->relativeUrlFor('about');
    about <- expected
    /public/about <- result
    
  • Using only $app->getRouteCollector()->setBasePath('/public'), RouteParser produces corrent urls for both urlFor and relativeUrlFor methods but breaks current path detection

  • Using both $app->setBasePath and $routeCollector->setBasePath produces invalid url paths for all methods

@l0gicgate
Copy link
Member

@piotr-cz that's an interesting bug. Let's open an issue for that.

@l0gicgate
Copy link
Member

I'm closing this as resolved

To set base path use:
$app->setBasePath('/base-path)

@hdkcreative
Copy link

thank you @adriansuter , work for me in URL : http://localhost/slim4/public/

and structure folder : Click here for view

I just installed Slim 4.0.0-alpha, configured an alias called slim4 to my virtual host localhost (Apache 2.4.34) pointing towards a subdirectory public of the installation and added the following .htaccess-file to that subdirectory (no RewriteBase needed)

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /slim4/index.php [QSA,L]

In the same subdirectory I then added a very simple index.php-file

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

include_once(__DIR__ . '/../vendor/autoload.php');

define('ROUTE_NAME_ANSWER', 'answer');

$app = AppFactory::create();
$app->setBasePath('/slim4');

$app->get('/', function (Request $request, Response $response, $args) use ($app) {
    $routeParser = $app->getRouteCollector()->getRouteParser();
    $response->getBody()->write('<a href="' . $routeParser->urlFor(ROUTE_NAME_ANSWER) . '">What is the answer?</a>');
    return $response;
});

$app->get('/answer', function (Request $request, Response $response, $args) {
    $response->getBody()->write('42');
    return $response;
})->setName(ROUTE_NAME_ANSWER);

$app->run();

Requesting http://localhost/slim4/ worked as expected. The link href was correctly set (so the route parser urlFor did prefix the base path) and http://localhost/slim4/answer worked also like a charm.

my htaccess:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /slim4/public/index.php [QSA,L]

my index.php ( the same of http://www.slimframework.com/)

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();

$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
$name = $args['name'];
$response->getBody()->write("Hello, $name");
return $response;
});

$app->run();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests