Как отобразить 404 и доработать роутер?

134
27 февраля 2021, 23:20

Делаю роутер с последующей иньекций роутов из бд, и вкурить никак не могу, как мне заставить работать страницу ошибки? Допустим 404. Надо, site.ru/абракадабра1111 выдало ошибку, по эррор документу в .htaccess и кинуло на /err (да я знаю что не перекинет на site.ru/err) но, почему не отрабатывает? Как раздебажить? Вар дампом все переменные прогнал, пытался встроить в foreach но при этом если страница существует в конфиге, всё равно отобразить код 404.

.htaccess

<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php [QSA,L]
    RewriteCond %{HTTP_HOST} ^www.site.ru$ [NC]
    RewriteRule ^(.*)$ https://site.ru/$1 [R=301,L]
    RewriteCond %{THE_REQUEST} /(.*)index.php.*$
    RewriteRule .* /%1 [R=301,L]
</IfModule>
RewriteEngine On
RewriteCond %{SERVER_PORT} ^80$
RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [R=301,L]
ErrorDocument 400 /err
ErrorDocument 401 /err
ErrorDocument 403 /err
ErrorDocument 404 /err
ErrorDocument 500 /err
ErrorDocument 503 /err

default.ini

[default_routes]
pMain='/';
admin='/admin';
login='/login';
logout='/logout';
error='/err';

class.router.php

<?PHP
require_once('class.router.paths.php');
class router {
    public function __construct() {
        $router_array = parse_ini_file('./data/router/default.ini', true);
        $routes = $router_array['default_routes'];
        $this->display($routes);
    }
    public function getRequestPath() {
        $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
        return '/' . ltrim(str_replace('index.php', '', $path), '/');
    }
    public function display($routes) {
        $path = $this->getRequestPath();
        $callf = new RouterPaths();
        foreach ($routes as $i => $routers) {
            if ($path === $routers) {
                $callf->$i();
            }
        }
    }
}
?>

class.router.paths.php

<?PHP
class RouterPaths {
    public function error() {
        $HttpStatus = $_SERVER["REDIRECT_STATUS"];
        $httpc = array(400,401,403,404,500,503);
        if ($error_page == null) {
            if (in_array($HttpStatus, $httpc)) {
                $error_page = $HttpStatus;
            } else {
                $error_page = 404;
            }
        } else if ($error_page == 'blocked') {
            $error_page = 'blocked';
        }
        if ($error_page == 400) {
            header('HTTP/1.0 400 Bad Request');
            header("HTTP/1.1 400 Bad Request");
            header("Status: 400 Bad Request");
            echo '400';
        } else if ($error_page == 401) {
            header('HTTP/1.0 401 Unauthorized');
            header("HTTP/1.1 401 Unauthorized");
            header("Status: 401 Unauthorized");
            echo '401';
        } else if ($error_page == 403) {
            header('HTTP/1.0 403 Forbidden');
            header("HTTP/1.1 403 Forbidden");
            header("Status: 403 Forbidden");
            echo '403';
        } else if ($error_page == 404) {
            header('HTTP/1.0 404 Not Found');
            header("HTTP/1.1 404 Not Found");
            header("Status: 404 Not Found");
            echo '404';
        } else if ($error_page == 500) {
            header('HTTP/1.0 500 Internal Server Error');
            header("HTTP/1.1 500 Internal Server Error");
            header("Status: 500 Internal Server Error");
            echo '500';
        } else if ($error_page == 503) {
            header('HTTP/1.0 503 Service Temporarily Unavailable');
            header('HTTP/1.1 503 Service Temporarily Unavailable');
            header('Status: 503 Service Temporarily Unavailable');
            echo '503';
        } else if ($error_page == 'blocked') {
            header('HTTP/1.0 403 Forbidden');
            header("HTTP/1.1 403 Forbidden");
            header("Status: 403 Forbidden");
            echo 'blocked';
        }
        exit;
    }
    public function pMain() {
        echo 'Main';
    }
    public function admin() {
        echo 'Admin';
    }
}
?>
Answer 1

Попробуйте пересмотреть ваше решение. Возьмите чистый лист бумаги (или откройте пустой файл в текстовом редакторе) и напишите что вы хотите сделать. Постарайтесь мыслить независимо от уже имеющихся у вас решений. Например, последующая инъекция роутов из бд звучит круто, но на самом деле не имеет отношения к маршрутизации.

Давайте попробуем вместе. Во-первых нужна маршрутизация, то есть возможность в вашем приложении запустить разные обработчики запросов в зависимости от формата этих запросов. Например, для запроса GET /login хочется запустить обработчик, который соберёт html-форму, а для запроса GET /logout обработчик, который удалит сессию пользователя и перенаправит его на главную страницу.

Получается, что нужно соответствие запрос -> обработчик. Похоже на то, что записано у вас в default.ini, только в другую сторону. Это соответствие можно хранить в ini-файле, в базе данных, в памяти и вообще любом месте. Но попадать в объект маршрутизатора оно должно уже в подготовленном виде (парсинг ini-файла в конструкторе делает ваш код совершенно негибким). Я думаю для начала соответствие запрос -> обработчик может быть одномерным ассоциативным массивом, где в ключе будет образец запроса, а в значении имя обработчика.

Так как мы говорим об образце запроса, о шаблоне, то на ум приходят регулярные выражения. Они очень удобны в случае, когда запрос содержит данные, которые надо из него извлечь. Например, GET /users/123, где 123 -- идентификатор пользователя. Регулярные выражения это строки и они могут быть ключами ассоциативного массива маршрутов.

[
    '~^GET /login$~' => 'login',
    '~^GET /logout$~' => 'logout',
    '~^GET /users/(?<id>\d*)$~' => 'user',
];

Обратите внимание, что регулярные выражения позволяют довольно элегантно распарсить строку запроса, именуя вхождения.

Теперь самое главное. Что должен делать роутер? Должен ли он добывать запрос из окружения? Должен ли отправлять ответ? Нет, не должен. Он должен только сопоставлять переданный ему запрос с образцами из массива и возвращать обработчик или null, если обработчик не удалось найти. Обратите внимание, что роутер даже не выполняет обработчик, он просто занимается маршрутизацией.

class Router {
    private $routes;
    public function __construct(array $routes) {
        $this->routes = $routes;
    }
    public function route(string $request): array {
        foreach ($this->routes as $pattern => $handler) {
            $matches = [];
            if (preg_match($pattern, $request, $matches) === 1) {
                $params = [];
                foreach ($matches as $name => $value) {
                    if (is_string($name)) {
                        $params[$name] = $value;
                    }
                }
                return [$handler, $params];
            }
        }
        return [null, []];
    }
}

Получился довольно простой класс. Его даже можно протестировать не заморачиваясь с созданием разных ini-файлов или замокиванием бд. Пользоваться им очень просто.

$method = $_SERVER['REQUEST_METHOD']?? 'UNKNOWN';
$url = $_SERVER['REQUEST_URI']?? '';
$path = parse_url($url, PHP_URL_PATH);
list($handler, $params) = $router->route("$method $path");

Если $handler === null, значит обработчик не найден и можно вернуть 404 ошибку. Обычно это делается так:

http_response_code(404);
echo 'Страница не найдена'; // тут можно вывести красивую страницу ошибки
exit(0);

После этого остаётся только выполнить обработчик и вывести результат. Тут есть простор для творчества, но мне нравится подход с подключением обработчиков из файлов (в таком случае возможна ситуация, когда маршрутизатор нашёл маршрут, но файла не существует, тогда можно тоже вернуть 404). Я покажу вам класс, который использую, надеюсь вы поймёте что к чему.

class Loader {
    private $dir;
    public function __construct(string $dir) {
        $this->dir = $dir;
    }
    public function load(string $path) {
        $fullPath = realpath($this->dir . '/' . $path);
        if (!$fullPath) {
            throw new \Exception('Не найден скрипт ' . $path);
        }
        $isSafe = (strpos($fullPath, $this->dir) === 0);
        if (!$isSafe) {
            throw new \Exception(
                'Скрипт "' . $path
                . '" за пределами директории "' . $this->dir . '"'
            );
        }
        if (!is_file($fullPath) || !is_readable($fullPath)) {
            throw new \Exception(
                'Скрипт "' . $fullPath . '" не является файлом доступным для чтения'
            );
        }
        $result = require $fullPath;
        return $result;
    }
}

Сами обработчики лежат в отдельной папке (например handlers) и возвращают что-нибудь callable.

return function() {
    return 'Hello world';
};
Answer 2

Решил добавлением условной переменной в цикле и валидации этой переменной после цикла

READ ALSO
Регулярное выражение для цен

Регулярное выражение для цен

Как создать подходящее регулярное выражение? Есть такие строки:

109
Как извлечь из строки нужные данные?

Как извлечь из строки нужные данные?

Есть переменная с таким содержанием:

101
Как получить все ID элементов?

Как получить все ID элементов?

На странице есть неопределенное количество ссылок вида:

140