PHP Slim 4 on subdomain. Problem with generating route: hostname

I have small website on webhosting subdomain. Locally everything works. After uploading to webhosting, project is running: https://cv.dubak.sk/

Content of routes:

$app->get('/', \App\Action\HomeAction::class)->setName('home');
$app->get('/en/', \App\Action\HomeEnAction::class)->setName('en');
$app->redirect('/en', '/en/', 301);

Printing out the routes to HTML from Twig template:

<!-- {{full_url_for('home')}} -->
<!-- {{full_url_for('en')}} -->

and I see:

<!-- https://cv.dubak.sk/ -->
<!-- https://cv.dubak.sk/en/ -->

After I go to english version of the website: https://cv.dubak.sk/en/

I receive 404. Routes content is:

<!-- https://cv.dubak.sk/en// -->
<!-- https://cv.dubak.sk/en//en/ -->

So during the route generating, the hostname: cv.dubak.sk/en/ was used instead of: cv.dubak.sk

The content of the .htaccess file:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

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

Any idea what should be changed?

All routes (except the home root) should be defined without a trailing / route pattern in order to work correctly. Can you try that first?

@odan No, it didn’t help. I changed my route to:

$app->get('/en', \App\Action\HomeEnAction::class)->setName('en');
$app->redirect('/en/', '/en', 301);

And this is content of my UrlGenerator function:

    public function fullUrlFor(
        string $routeName,
        array $data = [],
        array $queryParams = []
    ): string {
        $url = $this->getRouteParser()->fullUrlFor(
            $this->getRequest()->getUri(),
            $routeName,
            $data,
            $queryParams
        );
        return $url;
    }
  1. You could now try to comment out or remove this line in the .htaccess file: RewriteBase /

  2. The Twig-View package for slim already provides such a helper function that also uses the correct request object internally through the middleware. Have you tried that build in function?

@odan I commented out the rule:
# RewriteBase /
and nothing has changed at: https://cv.dubak.sk/en
Output for routes:

<!-- {{full_url_for('home')}} -->
<!-- {{full_url_for('en')}} -->
<!-- {{url_for('home')}} -->
<!-- {{url_for('en')}} -->

is still the same:

<!-- https://cv.dubak.sk/en/ -->
<!-- https://cv.dubak.sk/en/en -->
<!-- /en/ -->
<!-- /en/en -->

The problem is, that locally everything works, so I am debugging it on production.

I am on PHP 8.2 and “slim/slim”: “^4.14”

  1. The Twig-View package for slim already provides such a helper function that also uses the correct request object internally through the middleware. Have you tried that build in function?

What do you mean by “build in” function?

I got an 404 error. Maybe the old redirect is still cached in your browser. Try to delete the browser cache.

Also make sure to use the exact 2 .htaccess files as shown here:

https://www.slimframework.com/docs/v4/start/web-servers.html

What do you mean by “build in” function?

@odan Yes, the 404 is the problem. Locally it’s working: http://localhost:8884/en is not throwing the 404 error.
On production server, where is it located as subdomain (folder: www/subdom/cv): https://cv.dubak.sk/en it throws 404. I am always reloading the page by using: Ctrl + Shift + R to clear the cache.

I am using build in function for generating URLs. I have: “slim/twig-view”: “^3.4” and I call it from Twig template as: {{ full_url_for(‘home’) }}

Both .htaccess files are configured correctly I believe.

I have UrlGenerator class in folder: src/Routing with this content:

<?php
namespace App\Routing;

use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\RouteParserInterface;
use Slim\Routing\RouteContext;
use UnexpectedValueException;

/**
 * Request sensitive URL generator.
 */
final class UrlGenerator
{
    /**
     * @var ServerRequestInterface|null
     */
    private $request;

    /**
     * The constructor.
     *
     * @param ServerRequestInterface|null $request The request
     */
    public function __construct(ServerRequestInterface $request = null)
    {
        $this->request = $request;
    }

    /**
     * Set request.
     *
     * @param ServerRequestInterface $request The request
     */
    public function setRequest(ServerRequestInterface $request): void
    {
        $this->request = $request;
    }

    /**
     * Get the request.
     *
     * @throws UnexpectedValueException
     *
     * @return ServerRequestInterface The request
     */
    private function getRequest(): ServerRequestInterface
    {
        if (!$this->request) {
            throw new UnexpectedValueException('The request is not defined');
        }

        return $this->request;
    }

    /**
     * Build the path for a named route including the base path.
     *
     * This method prepares the response object to return an HTTP Redirect
     * response to the client.
     *
     * @param string $routeName The route name
     * @param array<mixed> $data Named argument replacement data
     * @param array<mixed> $queryParams Optional query string parameters
     *
     * @return string The url
     */
    public function urlFor(
        string $routeName,
        array $data = [],
        array $queryParams = []
    ): string {
        return $this->getRouteParser()->urlFor($routeName, $data, $queryParams);
    }

    /**
     * Build the path for a named route including the base path.
     *
     * This method prepares the response object to return an HTTP Redirect
     * response to the client.
     *
     * @param string $routeName The route name
     * @param array<mixed> $data Named argument replacement data
     * @param array<mixed> $queryParams Optional query string parameters
     *
     * @return string The url
     */
    public function fullUrlFor(
        string $routeName,
        array $data = [],
        array $queryParams = []
    ): string {
        return $this->getRouteParser()->fullUrlFor(
            $this->getRequest()->getUri(),
            $routeName,
            $data,
            $queryParams
        );
    }

    /**
     * Get route parser.
     *
     * @throws UnexpectedValueException
     *
     * @return RouteParserInterface The route parser
     */
    private function getRouteParser(): RouteParserInterface
    {
        if (!$this->request) {
            throw new UnexpectedValueException('The request is not defined');
        }

        return RouteContext::fromRequest($this->request)->getRouteParser();
    }
}

and this class is used in responder:

<?php
namespace App\Responder;

use App\Routing\UrlGenerator;
use JsonException;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Slim\Views\Twig;

/**
 * A generic responder.
 */
final class Responder
{
    /**
     * @var Twig
     */
    private $twig;

    /**
     * @var UrlGenerator
     */
    private $urlGenerator;

    /**
     * @var ResponseFactoryInterface
     */
    private $responseFactory;

    /**
     * The constructor.
     *
     * @param Twig $twig The twig engine
     * @param UrlGenerator $urlGenerator The url generator
     * @param ResponseFactoryInterface $responseFactory The response factory
     */
    public function __construct(
        Twig $twig,
        UrlGenerator $urlGenerator,
        ResponseFactoryInterface $responseFactory
    ) {
        $this->twig = $twig;
        $this->urlGenerator = $urlGenerator;
        $this->responseFactory = $responseFactory;
    }

should I adjust these classes?

This Twig-View package uses its own url generator. So this class should not be the issue. You may need to add the Slim routing mideware. But without seeing the code its not so easy to help here.

@odan Please let me know, if any file is missing.
config/middleware.php:

<?php
declare(strict_types=1);

use Slim\App;
use Slim\Middleware\ErrorMiddleware;
use App\Middleware\UrlGeneratorMiddleware;
use Selective\BasePath\BasePathMiddleware;
use Slim\Views\TwigMiddleware;
use \Slim\HttpCache\Cache;
// use Middlewares;

return function (App $app) {
    // Parse json, form data and xml
    $app->addBodyParsingMiddleware();

    $app->add(UrlGeneratorMiddleware::class);

    // Add the Slim built-in routing middleware
    $app->addRoutingMiddleware();

    // Catch exceptions and errors
    $app->add(ErrorMiddleware::class);    

    $app->add(TwigMiddleware::class);

    $app->add(BasePathMiddleware::class);

    // Register the http cache middleware.
    $app->add(new Cache('public', 600));

    // Middlewares::TrailingSlash(true) //(optional) set true to add the trailing slash instead remove
    //     ->redirect(301);
};

config/routes.php:

<?php
declare(strict_types=1);

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;

return function (App $app) {
    $app->get('/', \App\Action\HomeAction::class)->setName('home');

    $app->get('/en', \App\Action\HomeEnAction::class)->setName('en');
    $app->redirect('/en/', '/en', 301);
    // listening on both with/withour trailing slash
    // $app->get('/en[/]', \App\Action\HomeEnAction::class)->setName('en');

    // redirects
    $app->redirect('/sk[/]', '/', 301);

    $app-> get('/download', \App\Action\DownloadAction::class)->setName('download');
};

src/Middleware/UrlGeneratorMiddleware.php:

<?php

namespace App\Middleware;

use App\Routing\UrlGenerator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * Middleware.
 */
final class UrlGeneratorMiddleware implements MiddlewareInterface
{
    /**
     * @var UrlGenerator
     */
    private $urlGenerator;

    /**
     * The constructor.
     *
     * @param UrlGenerator $urlGenerator The url generator
     */
    public function __construct(UrlGenerator $urlGenerator)
    {
        $this->urlGenerator = $urlGenerator;
    }

    /**
     * Invoke middleware.
     *
     * @param ServerRequestInterface $request The request
     * @param RequestHandlerInterface $handler The handler
     *
     * @return ResponseInterface The response
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $this->urlGenerator->setRequest($request);

        return $handler->handle($request);
    }
}

config/settings.php:

<?php
declare(strict_types=1);

// Error reporting for production
error_reporting(0);
// error_reporting(E_ALL);
ini_set('display_errors', '0');

// Timezone
date_default_timezone_set('Europe/Bratislava');

// Settings
$settings = [];

// Path settings
$settings['root'] = dirname(__DIR__);
//echo $settings['root'];
$settings['temp'] = $settings['root'] . '/tmp';
$settings['public'] = $settings['root'] . '/public';

// Error Handling Middleware settings
$settings['error'] = [
    // Should be set to false in production
    'display_error_details' => false,

    // Parameter is passed to the default ErrorHandler
    // View in rendered output by enabling the "displayErrorDetails" setting.
    // For the console and unit tests we also disable it
    'log_errors' => true,

    // Display error details in error log
    'log_error_details' => true,
];

// Application settings
$settings['app'] = [
    'secret' => '{{app_secret}}',
];

// Logger settings
$settings['logger'] = [
    'name' => 'app',
    'path' => $settings['root'] . '/app_logs',
    'filename' => 'app.log',
    'level' => \Monolog\Logger::DEBUG,
    'file_permission' => 0775,
];

// Twig settings
// Configuration reference: https://symfony.com/doc/current/reference/configuration/twig.html
$settings['twig'] = [
    // Template paths
    'paths' => [
        __DIR__ . '/../templates',
        __DIR__ . '/../public',
    ],
    // Twig environment options
    'options' => [
        'debug' => false,
        // Should be set to true in production
        'cache_enabled' => true,
        'cache_path' => $settings['temp'] . '/twig',
    ],
];

// Assets
$settings['assets'] = [
    // Public assets cache directory
    'path' => $settings['public'] . '/cache',
    'url_base_path' => 'cache/',
    // Cache settings
    'cache_enabled' => true,
    'cache_path' => $settings['temp'],
    'cache_name' => 'assets-cache',
    //  Should be set to 1 (enabled) in production
    'minify' => 1,
];

// Session
$settings['session'] = [
    'name' => 'dubakcv',
    'cache_expire' => 0,
];

// Console commands
$settings['commands'] = [
    \App\Console\TwigCompilerCommand::class
];

return $settings;

config/container.php:

<?php
declare(strict_types=1);

use App\Factory\LoggerFactory;
use App\Handler\DefaultErrorHandler;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Fullpipe\TwigWebpackExtension\WebpackExtension;
use Selective\BasePath\BasePathMiddleware;
use Selective\Validation\Encoder\JsonEncoder;
use Selective\Validation\Middleware\ValidationExceptionMiddleware;
use Selective\Validation\Transformer\ErrorDetailsResultTransformer;
use Slim\App;
use Slim\Factory\AppFactory;
use Slim\Http\Environment;
use Slim\Interfaces\RouteParserInterface;
use Slim\Middleware\ErrorMiddleware;
use Slim\Psr7\Factory\UriFactory;
use Slim\Views\Twig;
use Slim\Views\TwigExtension;
use Slim\Views\TwigMiddleware;
use Slim\Views\TwigRuntimeLoader;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFunction;
use Odan\Twig\TwigAssetsExtension;
use \Slim\HttpCache\CacheProvider;

return [
    'settings' => function () {
        return require __DIR__ . '/settings.php';
    },

    'environment' => function() {        
            // $_SERVER["SERVER_NAME"] = "cv.dubak.sk";
            return new Environment($_SERVER);
    },

    App::class => function (ContainerInterface $container) {
        AppFactory::setContainer($container);

        return AppFactory::create();
    },

    // For the responder
    ResponseFactoryInterface::class => function (ContainerInterface $container) {
        return $container->get(App::class)->getResponseFactory();
    },

    // The Slim RouterParser
    RouteParserInterface::class => function (ContainerInterface $container) {
        return $container->get(App::class)->getRouteCollector()->getRouteParser();
    },

     // The logger factory
     LoggerFactory::class => function (ContainerInterface $container) {
        return new LoggerFactory($container->get('settings')['logger']);
    },

    TwigMiddleware::class => function (ContainerInterface $container) {
        return TwigMiddleware::createFromContainer($container->get(App::class), Twig::class);
    },

    Twig::class => function (ContainerInterface $container) {
        // $settings = $container->get('settings')['twig'];

        $config = (array)$container->get('settings');
        $settings = $config['twig'];
        $twig = Twig::create($settings['paths'], $settings['options']);


        $loader = $twig->getLoader();
        $publicPath = (string)$config['public'];
        if ($loader instanceof FilesystemLoader) {
            $loader->addPath($publicPath, 'public');
        }

        // Add extensions
        $twig->addExtension(new WebpackExtension($publicPath . '/assets/manifest.json', $publicPath));

        $environment = $twig->getEnvironment();

        // Add Twig extensions
        $twig->addExtension(new TwigAssetsExtension($environment, (array)$config['assets']));

        // $view = new Twig(__DIR__ . '/../templates', [
        //     'cache' => false,
        // ]);
        // extension for path_for and base_url
        // $twig->addExtension(new TwigExtension($container->router, $container->request->getUri()));

        return $twig;
    },

    ErrorMiddleware::class => function (ContainerInterface $container) {
        $settings = $container->get('settings')['error'];
        $app = $container->get(App::class);

        $logger = $container->get(LoggerFactory::class)
            ->addFileHandler('error.log')
            ->createInstance('default_error_handler');

        $errorMiddleware = new ErrorMiddleware(
            $app->getCallableResolver(),
            $app->getResponseFactory(),
            (bool)$settings['display_error_details'],
            (bool)$settings['log_errors'],
            (bool)$settings['log_error_details'],
            $logger
        );

        $errorMiddleware->setDefaultErrorHandler($container->get(DefaultErrorHandler::class));

        return $errorMiddleware;
    },    

    BasePathMiddleware::class => function (ContainerInterface $container) {
        $app = $container->get(App::class);

        return new BasePathMiddleware($app);
    },

    CacheProvider::class => function (ContainerInterface $container) {
        $app = $container->get(App::class);

        return new CacheProvider($app);
    }
];

Also, worthy to note, that website routing worked before the upgrade from Slim 4.7 onto 4.14.

First, Slim 4 middleware is LIFO, so you need to refactor that order like this, otherwise the error handling and routing for Twig will not work correctly.

$app->add(new Cache('public', 600)); // Better use Cache::class instead and add a DI container entry for that.
$app->add(UrlGeneratorMiddleware::class);
$app->add(TwigMiddleware::class);
$app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();
$app->add(BasePathMiddleware::class);
$app->add(ErrorMiddleware::class);    
  1. The DI-Container is not HTTP context specific, so there should be no HTTP specific values such used, such as $_SERVER, $_GET, $_POST etc…
    This DI container entry for Environment is not optimal, better prevent two instances of that class Environment in your App, because the Twig-View object already comes with it own Environment object that uses the PSR-7 request object. Otherwise it can cause routing and url issues.

‘environment’ => function() {
// $_SERVER[“SERVER_NAME”] = “cv.dubak.sk”;
return new Environment($_SERVER);
},

Example:

Environment::class => function (ContainerInterface $container) {
    $twig = $container->get(Twig::class);
    return $twig->getEnvironment();
}

@odan I implemented changes into: config/container.php and middleware.php files and now I see HTTP 500 error:

Uncaught Twig\Error\RuntimeError: Unable to load the "Slim\Views\TwigRuntimeExtension" runtime. in /templates/layout/layout.twig:41

which is place where is {{ full_url_for(‘home’) }} called

It looks like, the Slim Twig-View component is not fully prepared.

Try to check the error log files or enabled error reporting details to see the exact error message.

Update:

Try this to fix it: Use the TwigMiddleware::create method, instead of createFromContainer.

TwigMiddleware::class => function (ContainerInterface $container) {
    $app = $container->get(App::class);
    $twig = $container->get(Twig::class);
    
    return TwigMiddleware::create($app, $twig);
},

PS: I created this bug report: TwigMiddleware::createFromContainer does not set request attribute "view" · Issue #322 · slimphp/Twig-View · GitHub

Any feedback? @dubaksk

@odan Well, still not working. The most interesting is, that I use pretty much the same code for this website: https://www.dubak.sk/en/ where switching between the English and Slovak versions is working.
It is not working for subdomain: https://cv.dubak.sk/
So it looks like Routing and generating URL has problems to handle the subdomain.
As I wrote at the beginning, URL domain is generated with wrong.

The Slim internal routing system should not be affected by the Domain, because it just handles the path of the URL. So maybe there is some special server configuration that should be checked.