PHP routing - How can I implement 404 page on wrong url routes? - php

everyone.
I have a basic router created in PHP.
I can redirect to any page I want, if there is a callback function the callback function gets executed and if there is a page (String instead of a function) the page loads the correct file. However I can't figure out how to implement 404 page on non-existing route.
I tried to reuse the preg_match() function, but that gave me no results and if I place the notFound() (404 page) in the else block, it always gets executed regardless of the correct url or not.
if(preg_match($pattern, $path, $matches) && $httpMethod === $route['method']) {
}else{
self::notFound(); //THIS GETS EXECUTED ON EVERY ROUTE
}
This is my Code.
<?php
class Router{
public static $routes = [];
public static function get($route, $callback){
self::$routes[] = [
'route' => $route,
'callback' => $callback,
'method' => 'GET'
];
}
public static function resolve(){
$path = $_SERVER['REQUEST_URI'];
$httpMethod = $_SERVER['REQUEST_METHOD'];
$methodMatch = false;
$routeMatch = false;
foreach(self::$routes as $route){
// convert urls like '/users/:uid/posts/:pid' to regular expression
$pattern = "#^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($route['route'])) . "$#D";
$matches = Array();
// check if the current request matches the expression
if(preg_match($pattern, $path, $matches) && $httpMethod === $route['method']) {
// remove the first match
array_shift($matches);
// call the callback with the matched positions as params
if(is_callable($route['callback'])){
call_user_func_array($route['callback'], $matches);
}else{
self::render($route['callback']);
}
}
}
}
public static function render($file, $viewsFolder='./views/'){
include($viewsFolder . $file);
}
public static function notFound(){
http_response_code(400);
include('./views/404.php');
exit();
}
}
Router::get("/", "home.php");
Router::get("/user/:id", function($val1) {
$data = array(
"Nicole",
"Sarah",
"Jinx",
"Sarai"
);
echo $data[$val1] ?? "No data";
});
Router::get("/user/profile/:id", "admin.php");
Router::resolve();
?>

You can add notFound() at the very end of resolve() method, and a return when you hit a match:
public static function resolve(){
$path = $_SERVER['REQUEST_URI'];
$httpMethod = $_SERVER['REQUEST_METHOD'];
$methodMatch = false;
$routeMatch = false;
foreach(self::$routes as $route){
$pattern = "#^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($route['route'])) . "$#D";
$matches = Array();
if(preg_match($pattern, $path, $matches) && $httpMethod === $route['method']) {
array_shift($matches);
if(is_callable($route['callback'])){
call_user_func_array($route['callback'], $matches);
}else{
self::render($route['callback']);
}
return;
}
}
notFound();
}

Related

Pass a variable to language file without sprintf

I want to pass a variable to a language file. I have created MY_language.php in application/core/MY_language.php.
class MY_Language extends CI_Lang
{
public function __construct()
{
parent::__construct();
}
function line($line, $params = null)
{
$return = parent::line($line);
if ($return === false) {
return "!-- $line --!";
} else {
if (!is_null($params)) {
$return = $this->_ni_line($return, $params);
}
return $return;
}
}
private function _ni_line($str, $params)
{
$return = $str;
$params = is_array($params) ? $params : array($params);
$search = array();
$cnt = 0;
foreach ($params as $param) {
$search[$cnt] = '/\\$' . ($cnt + 1) . '/';
$cnt++;
}
$return = preg_replace($search, $params, $return);
return $return;
}
}
This file must override the CodeIgniter line() function and accept an array of parameters as input, and insert into string language everywhereIi have type $ in my language text.
$lang['delete'] = "$name was deleted";
The result of the above code is:
sam was deleted
in codeigniter 3 the language your core language file must be PREFIX_lang
Since you are adding parameters to the line() function you are unable to override it.
Use a different name like magic_line()

How to get an ID from an URL using preg_match?

Problem
I am currently doing a preg_match on an url. This url has a certain id in the second parameter or the third parameter. However I don't know how I could get this more efficiently.
Code
preg_match('~http://www.example.com/some/(.+?)/~is', $url, $id);
if (!isset($id[1])) {
preg_match('~http://www.example.com/some/thing/(.+?)/~is', $url, $id);
if (!isset($id[1])) {
preg_match('~http://www.example.com/some/other/(.+?)/~is', $url, $id);
if (!isset($id[1])) {
preg_match('~http://www.example.com/some/thingelse/(.+?)/~is', $url, $id);
if (!isset($id[1])) {
return false
}
}
}
}
What I would like to do
if (preg_match('~http://www.example.com/some/(.+?)/~is', $url, $id)) {
$id = $id[1];
} else if (preg_match('~http://www.example.com/some/(.+?)/(.+?)/~is', $url, $id)) {
$id = $id[1];
} else {
return false;
}
However, this doesn't seem to work.
If the following regular expressions in fact did work as you wanted them to
if (preg_match('~http://www.example.com/some/(.+?)/~is', $url, $id)) {
$id = $id[1];
} else if (preg_match('~http://www.example.com/some/(.+?)/(.+?)/~is', $url, $id)) {
$id = $id[1];
} else {
return false;
}
... then you would never reach the second case anyway. The match will already be made in the first RegEx, as the beginning or the second expression is identical to the first expression. And even if you turned them around you would always get the id from the first parameter/path part, as you set $id = $id[1] on both results.
As stated in the comments, you probably would be better off using parse_url for this instead:
$urls = [
'http://www.example.com/some/thingelse/foo/bar/baz/',
'http://www.example.com/some/foo/bar/baz/',
];
foreach ($urls as $url) {
echo "Checking $url", PHP_EOL;
$path = parse_url($url, PHP_URL_PATH);
$parts = explode('/', $path);
echo "Second parameter: ", $parts[2], PHP_EOL;
echo "Third parameter: ", $parts[3], PHP_EOL;
}
Output:
Checking http://www.example.com/some/thingelse/foo/bar/baz/
Second parameter: thingelse
Third parameter: foo
Checking http://www.example.com/some/foo/bar/baz/
Second parameter: foo
Third parameter: bar

Zend Framework create user-friendly url

I want create user-friendly URL like this:
mysite.com/flat-sale/london/1-room/1
When parts of URL is parameters:
flat-sale is post/list/type/1
london is city/123
1-room is rooms/1
and 1 id page/1
For doing this I created table in database - url_alias. This table has three column:
aid,url,alis
I inserted in this table next rows:
1 post/list/type/1 flat-sale
2 city/123 lonodon
3 1-room rooms/1
I am using Controller_Plugin for parsing URL:
class My_Controller_Plugin_UrlAlias extends Zend_Controller_Plugin_Abstract {
public function routeStartup(Zend_Controller_Request_Abstract $request) {
$alias = substr($request->getRequestUri(), 1);
$pattern = "([^/]+)";
//this model for CRUD from tables url_alias
$resources = new Admin_Resource_Materialalias();
$match = array();
if (preg_match_all($pattern, $alias, $match)) {
$url = array();
foreach($match['0'] as $m) {
//this is page
if (preg_match("#^[\d]+$#", $m)) {
$url[] = "page/$m";
} else {
$url[] = $resources->getUrl($m);
}
}
$url = implode("/", $url);
//echo $url;
}
if (isset($url) && strlen($url)) {
$request->setRequestUri($url);
}
}
}
This plugin is work perfectly.
But I am else needing create url like this: mysite.com/flat-sale/london/1-room/1.
For this purpose, I created new View_Helper:
class My_View_Helper_Alias extends Zend_View_Helper_Url {
public function alias(array $urlOptions = array(), $name = null, $reset = false, $encode = true) {
$url = $this->url($urlOptions, $name, $reset, $encode);
$pattern = "#([^/]+)\/([^/]+)\/([-a-zA-Z0-9_/.]+)#";
$params_pattern = "#([^/]+\/[\d]+)#";
if (preg_match($pattern, $url, $match)) {
$resources = new Admin_Resource_Materialalias();
if (preg_match_all($params_pattern, $match[3],$params)) {
$p_alias = array();
foreach($params[0] as $p) {
//add controller, action and first params
if (empty($p_alias)) {
$p = "/".$match[1].'/'.$match[2]."/".$p;
}
//this is page
if (preg_match("#page\/([\d]+)#", $p, $page)) {
$p_alias[] = $page[1];
continue;
}
//this model for CRUD from tables url_alias
$part = $resources->getAlias($p);
$p_alias[] = strlen($part)?$part:$p;
}
$alias = implode("/",$p_alias);
}
}
$alias = strlen($alias)? $alias : $url;
return $alias;
}
}
This view helper also work, but i think it is not optimal. Can anybody comments this code or maybe has same task? Thank you.
The problem is solved so, thanks for KA_lin:
routes.flat_sale_city_rooms.route = /:type/:city/:rooms/:page
routes.flat_sale_city_rooms.defaults.module = main
routes.flat_sale_city_rooms.defaults.controller = post
routes.flat_sale_city_rooms.defaults.action = list
routes.flat_sale_city_rooms.reqs.type = [^/]+
routes.flat_sale_city_rooms.reqs.city = [^/]+
routes.flat_sale_city_rooms.reqs.rooms = [^/]+
routes.flat_sale_city_rooms.defaults.page = 1
routes.flat_sale_city_rooms.reqs.page = \d+
routes.flat_sale_city.route = /:type/:city/:page
routes.flat_sale_city.defaults.module = main
routes.flat_sale_city.defaults.controller = post
routes.flat_sale_city.defaults.action = list
routes.flat_sale_city.reqs.type = [^/]+
routes.flat_sale_city.reqs.city = [^/]+
routes.flat_sale_city.defaults.page = 1
routes.flat_sale_city.reqs.page = \d+
Try in Bootstrap.php to add something like:
$route = new Zend_Controller_Router_Route (
'user/summary/:id/connection',
array('controller' => 'user',
'action' => 'get-summary',
'id' => FALSE,
)
);
$router->addRoute('equity', $route);
And this creates custom url`s you can call:
user/my-personal-url,
user/my-personal-url/connection
Assuming Zend 1 is used
Why don't you use Zend Framework routing? Have a look to this example: http://framework.zend.com/manual/2.0/en/user-guide/routing-and-controllers.html
you can use routing for user friendly urls
have a look for example
http://framework.zend.com/manual/1.5/en/zend.controller.router.html

Url routing regex PHP

I am writing a class that handles routing of my PHP webservice but I need to correct the regex, and I want to know what would be the most effecient way to parse the url.
Example urls:
POST /users
GET /users
GET /users&limit=10&offset=0
GET /users/search&keyword=Richard
GET /users/15/posts/38
What I want to create in PHP for class is this:
$router = new Router();
$router->addRoute('POST', '/users', function(){});
$router->addRoute('GET', '/users/:uid/posts/:pid', function($uid, $pid){});
$target = $router->doRouting();
The target variable would now contain an array with:
method
url
callback method
This is what I got so far:
class Router{
use Singleton;
private $routes = [];
private $routeCount = 0;
public function addRoute($method, $url, $callback){
$this->routes[] = ['method' => $method, 'url' => $url, 'callback' => $callback];
$this->routeCount++;
}
public function doRouting(){
$reqUrl = $_SERVER['REQUEST_URI'];
$reqMet = $_SERVER['REQUEST_METHOD'];
for($i = 0; $i < $this->routeCount; $i++){
// Check if the url matches ...
// Parse the arguments of the url ...
}
}
}
So I need a regex that first of all:
/mainAction/:argumentName/secondaryAction/:secondaryActionName
checks if that matches the $reqUrl (see at the for loop above)
Extracts the arguments, so we can use them in our callback function.
What I tried myself:
(code should be in the for loop # doRouting function)
// Extract arguments ...
$this->routing[$i]['url'] = str_replace(':arg', '.+', $this->routing[$i]['url']);
// Does the url matches the routing url?
if(preg_match('#^' . $this->routes[$i]['url'] . '$#', $reqUrl)){
return $this->routes[$i];
}
I really appreciate all help, thanks alot.
this basicly works now.
public function doRouting(){
// I used PATH_INFO instead of REQUEST_URI, because the
// application may not be in the root direcory
// and we dont want stuff like ?var=value
$reqUrl = $_SERVER['PATH_INFO'];
$reqMet = $_SERVER['REQUEST_METHOD'];
foreach($this->routes as $route) {
// convert urls like '/users/:uid/posts/:pid' to regular expression
$pattern = "#^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($route['url'])) . "$#D";
$matches = Array();
// check if the current request matches the expression
if($reqMet == $route['method'] && preg_match($pattern, $reqUrl, $matches)) {
// remove the first match
array_shift($matches);
// call the callback with the matched positions as params
return call_user_func_array($route['callback'], $matches);
}
}
}
PS: You dont need the $routeCount attribute
Great answer #MarcDefiant. Cleanest PHP router I came across. Did a small modification to support regular expression as well. Not sure why you use preq_quote ?
Small todo would be to cast the array to a assoc array. E.g. replace ['0' => 1] with ['id' => 1]
function matchRoute($routes = [], $url = null, $method = 'GET')
{
// I used PATH_INFO instead of REQUEST_URI, because the
// application may not be in the root direcory
// and we dont want stuff like ?var=value
$reqUrl = $url ?? $_SERVER['PATH_INFO'];
$reqMet = $method ?? $_SERVER['REQUEST_METHOD'];
$reqUrl = rtrim($reqUrl,"/");
foreach ($routes as $route) {
// convert urls like '/users/:uid/posts/:pid' to regular expression
// $pattern = "#^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($route['url'])) . "$#D";
$pattern = "#^" . preg_replace('/:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', $route['url']) . "$#D";
// echo $pattern."\n";
$params = [];
// check if the current request params the expression
$match = preg_match($pattern, $reqUrl, $params);
if ($reqMet == $route['method'] && $match) {
// remove the first match
array_shift($params);
// call the callback with the matched positions as params
// return call_user_func_array($route['callback'], $params);
return [$route, $params];
}
}
return [];
}
$match = matchRoute([
[
'method' => 'GET',
'url' => '/:id',
'callback' => function($req) {
exit('Hello');
}
],
[
'method' => 'GET',
'url' => '/api/(.*)', // Match all /api/hello/test/...
'callback' => function($req) {
print_r($req);
exit('cool');
}
]
]);
list($route,$params) = $match;
call_user_func_array($route['callback'], [$params]);

CakePhp: Url based internationalization

I've a small problem with my internationalization:
I want to have some url looking like this: http://mywebsite/eng/controller/action/params...
I found this http://nuts-and-bolts-of-cakephp.com/2008/11/28/cakephp-url-based-language-switching-for-i18n-and-l10n-internationalization-and-localization/
This is working nice most of time. But I've one case where this hasn't the expected result.
When I'm using $this->Html->link with named parameters, I don't get my nice structure, but something like http://mywebsite/controller/action/paramX:aaa/paramxY:bbb/language:eng
I think this is a routing problem, but I can't figure what is going wrong?
Thank you very much
This is because cakephp doens't find a route in routes.php that corresponds to this link. In other words, you'll have to define this route in the routes.php file
Router::connect('/:language/:controller/:action/:paramX/:paramY');
Once this set, $this->Html->link will output a nice url
I finally did this:
I created a custom CakeRoute, in this cakeRoute, I override the "match" url and the _writeUrl method.
Now every thing is working like a charm :)
For those which are interessted by the route class:
<?php
class I18nRoute extends CakeRoute {
/**
* Constructor for a Route
* Add a regex condition on the lang param to be sure it matches the available langs
*
* #param string $template Template string with parameter placeholders
* #param array $defaults Array of defaults for the route.
* #param string $params Array of parameters and additional options for the Route
* #return void
* #access public
*/
public function __construct($template, $defaults = array(), $options = array()) {
//$defaults['language'] = Configure::read('Config.language');
$options = array_merge((array)$options, array(
'language' => join('|', Configure::read('Config.languages'))
));
parent::__construct($template, $defaults, $options);
}
/**
* Attempt to match a url array. If the url matches the route parameters + settings, then
* return a generated string url. If the url doesn't match the route parameters false will be returned.
* This method handles the reverse routing or conversion of url arrays into string urls.
*
* #param array $url An array of parameters to check matching with.
* #return mixed Either a string url for the parameters if they match or false.
* #access public
*/
public function match($url) {
if (empty($url['language'])) {
$url['language'] = Configure::read('Config.language');
}
if (!$this->compiled()) {
$this->compile();
}
$defaults = $this->defaults;
if (isset($defaults['prefix'])) {
$url['prefix'] = $defaults['prefix'];
}
//check that all the key names are in the url
$keyNames = array_flip($this->keys);
if (array_intersect_key($keyNames, $url) != $keyNames) {
return false;
}
$diffUnfiltered = Set::diff($url, $defaults);
$diff = array();
foreach ($diffUnfiltered as $key => $var) {
if ($var === 0 || $var === '0' || !empty($var)) {
$diff[$key] = $var;
}
}
//if a not a greedy route, no extra params are allowed.
if (!$this->_greedy && array_diff_key($diff, $keyNames) != array()) {
return false;
}
//remove defaults that are also keys. They can cause match failures
foreach ($this->keys as $key) {
unset($defaults[$key]);
}
$filteredDefaults = array_filter($defaults);
//if the difference between the url diff and defaults contains keys from defaults its not a match
if (array_intersect_key($filteredDefaults, $diffUnfiltered) !== array()) {
return false;
}
$passedArgsAndParams = array_diff_key($diff, $filteredDefaults, $keyNames);
list($named, $params) = Router::getNamedElements($passedArgsAndParams, $url['controller'], $url['action']);
//remove any pass params, they have numeric indexes, skip any params that are in the defaults
$pass = array();
$i = 0;
while (isset($url[$i])) {
if (!isset($diff[$i])) {
$i++;
continue;
}
$pass[] = $url[$i];
unset($url[$i], $params[$i]);
$i++;
}
/*
//still some left over parameters that weren't named or passed args, bail.
//We don't want this behavior, we use most of args for the matching, and if we have more, we just allow them as parameters
if (!empty($params)) {
return false;
}*/
//check patterns for routed params
if (!empty($this->options)) {
foreach ($this->options as $key => $pattern) {
if (array_key_exists($key, $url) && !preg_match('#^' . $pattern . '$#', $url[$key])) {
return false;
}
}
}
return $this->_writeUrl(array_merge($url, compact('pass', 'named')));
}
function _writeUrl($params) {
if (isset($params['prefix'], $params['action'])) {
$params['action'] = str_replace($params['prefix'] . '_', '', $params['action']);
unset($params['prefix']);
}
if (is_array($params['pass'])) {
$params['pass'] = implode('/', $params['pass']);
}
$instance =& Router::getInstance();
$separator = $instance->named['separator'];
if (!empty($params['named']) && is_array($params['named'])) {
$named = array();
foreach ($params['named'] as $key => $value) {
$named[] = $key . $separator . $value;
}
$params['pass'] = $params['pass'] . '/' . implode('/', $named);
}
$out = $this->template;
$search = $replace = array();
foreach ($this->keys as $key) {
$string = null;
if (isset($params[$key])) {
$string = $params[$key];
} elseif (strpos($out, $key) != strlen($out) - strlen($key)) {
$key .= '/';
}
$search[] = ':' . $key;
$replace[] = $string;
}
$out = str_replace($search, $replace, $out);
if (strpos($this->template, '*')) {
$out = str_replace('*', $params['pass'], $out);
}
$out = str_replace('//', '/', $out);
//Modified part: allows us to print unused parameters
foreach($params as $key => $value){
$found = false;
foreach($replace as $repValue){
if($value==$repValue){
$found=true;
break;
}
}
if(!$found && !empty($value)){
$out.="/$key:$value";
}
}
return $out;
}
}
And you can set the route like this:
Router::connect('/:language/:controller/*', array(), array('routeClass' => 'I18nRoute'));

Categories