preg replace doesn't replace right adds an extra backslash - php

Good day everyone. I have the following two functions one for adding a rule and the other one for matching that rule. The problem is that when i use two params one of them doesn't get changed and i don't understand why it doesn't work. any help is apreciated.
public function add($name, $pattern, $controller, $action = null, array $params = array())
{
if(!isset($this->routeCollection[$name]))
$this->routeCollection[$name] =
array(
'pattern' => $pattern,
'controller' => $controller,
'action' => $action,
'params' => $params,
);
}
public function findMatch($url)
{
foreach ($this->routeCollection as $routeMap) {
$this->regex = $this->buildRegex($routeMap['pattern'], $routeMap['params']);
// Let's test the route.
if (preg_match($this->regex, $url)) {
return array('controller' => $routeMap['controller'], 'action' => $routeMap['action']);
}
}
return array('controller' => $this->routeCollection['404']['controller'], 'action' => $this->routeCollection['404']['action']);
}
public function buildRegex($uri, array $params)
{
// Find {params} in URI
if (preg_match_all('/\{(?:[^\}]+)\}/', $uri, $this->matches, PREG_SET_ORDER)) {
foreach ($this->matches as $isMatch) {
// Swap {param} with a placeholder
$this->uri = str_replace($isMatch, "%s", $uri);
}
// Build final Regex
$this->finalRegex = '/^' . preg_quote($this->uri, '/') . '$/';
$this->finalRegex = vsprintf($this->finalRegex, $params);
var_dump($this->finalRegex);
} else {
$this->finalRegex = '/^(' . preg_quote($uri, '/') . ')$/';
$this->finalRegex = str_replace(array('\.', '\-'), array('.', '-'), $this->finalRegex);
}
return $this->finalRegex;
}
// Usage:
$routeCollection->add('CatalogByCategory', '/catalog/category/{slugLink}', 'Ex:Controller:Catalog', 'ViewByCategory',
array('slugLink' => ('[a-z0-9]+(?:-[a-z0-9]+)*') ));
$routeCollection->add('ListCatalogPageByCategory', '/catalog/category/{sluglinks}/{pageNumber}', 'Ex:Controller:Catalog', 'ListCatalog',
array('sluglinks' => ('[a-z0-9]+(?:-[a-z0-9]+)*'), 'pageNumber' => ('[1-9][0-9]*') ));
// From Dump:
string '/^\/catalog\/category\/[a-z0-9]+(?:-[a-z0-9]+)*$/' (length=49)
string '/^\/catalog\/category\/\{sluglinks\}\/[a-z0-9]+(?:-[a-z0-9]+)*$/' (length=64)

foreach ($this->matches as $isMatch) {
// Swap {param} with a placeholder
$this->uri = str_replace($isMatch, "%s", $uri);
}
You keep overwriting $this->uri with the value of $uri being run through a replacement - in this case, it's getting set with {sluglinks} being replaced, then set again with only {pageNumber} being replaced.
You should use $this->uri = $uri;, and then always use $this->uri.

Related

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

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

Illegal offset type in my php class function

I am getting illegal offset error in one of my function.
Here is the code
public function translate($str, $namespace = '') {
$namespace = $this->getNamespace($namespace);
$this->load($namespace);
$lang = $this->getLang();
$data = $this->data[$namespace];
$key = md5($str);
if (isset($data[$lang][$key])) {
return $data[$lang][$key];
}
$default = $this->getDefaultLang();
if ($lang == $default) {
if ($this->getSaveOnMissing()) {
$save = array(
'namespace' => $namespace,
'lang' => $lang,
'trans_key' => $key,
'trans_val' => $str
);
$sql = $GLOBALS['db']->strInsert('translate', $save);
if ($GLOBALS['db']->SQL_query($sql)) {
$this->data[$namespace][$lang][$key] = $str;
}
}
return $str;
} elseif (isset($data[$default][$key])) {
return $data[$default][$key];
}
return $str;
}
In this line I am getting this error
elseif (isset($data[$default][$key]))
So there are 3 levels of your value
$data
$default
$key
You can check by three levels first if they are all returning something
elseif(isset($data, $data[$default], $data[$default][$key])

How to do URL matching regex for routing framework?

I already have a routing method that matches this pattern:
/hello/:name
that set name to be a dynamic path, I want to know how to make it:
/hello/{name}
with the same regex. How to add optional trailing slash to it, like this?
/hello/:name(/)
or
/hello/{name}(/)
This is the regex I use for /hello/:name
#^/hello/([a-zA-Z0-9\-\_]+)$#D
The regex is auto generated from PHP class
private function getRegex($pattern){
$patternAsRegex = "#^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($pattern)) . "$#D";
return $patternAsRegex;
}
If the route is /hello/:name(/) I want it to make the match with optional thing else continue normal
This will create a regular expression for the $pattern route with both :name and {name} parameters, as well as the optional slash. As a bonus, it will also add a ?<name> to make the parameter easier to handle down the line.
For example, a route pattern of /hello/:name(/) will get the regular expression #^/hello/(?<name>[a-zA-Z0-9\_\-]+)/?$#D. When matched with a URL, like preg_match( <regex above>, '/hello/sarah', $matches) that would give you $matches['name'] == 'sarah'.
There are some tests to be found below the actual function.
function getRegex($pattern){
if (preg_match('/[^-:\/_{}()a-zA-Z\d]/', $pattern))
return false; // Invalid pattern
// Turn "(/)" into "/?"
$pattern = preg_replace('#\(/\)#', '/?', $pattern);
// Create capture group for ":parameter"
$allowedParamChars = '[a-zA-Z0-9\_\-]+';
$pattern = preg_replace(
'/:(' . $allowedParamChars . ')/', # Replace ":parameter"
'(?<$1>' . $allowedParamChars . ')', # with "(?<parameter>[a-zA-Z0-9\_\-]+)"
$pattern
);
// Create capture group for '{parameter}'
$pattern = preg_replace(
'/{('. $allowedParamChars .')}/', # Replace "{parameter}"
'(?<$1>' . $allowedParamChars . ')', # with "(?<parameter>[a-zA-Z0-9\_\-]+)"
$pattern
);
// Add start and end matching
$patternAsRegex = "#^" . $pattern . "$#D";
return $patternAsRegex;
}
// Test it
$testCases = [
[
'route' => '/hello/:name',
'url' => '/hello/sarah',
'expectedParam' => ['name' => 'sarah'],
],
[
'route' => '/bye/:name(/)',
'url' => '/bye/stella/',
'expectedParam' => ['name' => 'stella'],
],
[
'route' => '/find/{what}(/)',
'url' => '/find/cat',
'expectedParam' => ['what' => 'cat'],
],
[
'route' => '/pay/:when',
'url' => '/pay/later',
'expectedParam' => ['when' => 'later'],
],
];
printf('%-5s %-16s %-39s %-14s %s' . PHP_EOL, 'RES', 'ROUTE', 'PATTERN', 'URL', 'PARAMS');
echo str_repeat('-', 91), PHP_EOL;
foreach ($testCases as $test) {
// Make regexp from route
$patternAsRegex = getRegex($test['route']);
if ($ok = !!$patternAsRegex) {
// We've got a regex, let's parse a URL
if ($ok = preg_match($patternAsRegex, $test['url'], $matches)) {
// Get elements with string keys from matches
$params = array_intersect_key(
$matches,
array_flip(array_filter(array_keys($matches), 'is_string'))
);
// Did we get the expected parameter?
$ok = $params == $test['expectedParam'];
// Turn parameter array into string
list ($key, $value) = each($params);
$params = "$key = $value";
}
}
// Show result of regex generation
printf('%-5s %-16s %-39s %-14s %s' . PHP_EOL,
$ok ? 'PASS' : 'FAIL',
$test['route'], $patternAsRegex,
$test['url'], $params
);
}
Output:
RES ROUTE PATTERN URL PARAMS
-------------------------------------------------------------------------------------------
PASS /hello/:name #^/hello/(?<name>[a-zA-Z0-9\_\-]+)$#D /hello/sarah name = sarah
PASS /bye/:name(/) #^/bye/(?<name>[a-zA-Z0-9\_\-]+)/?$#D /bye/stella/ name = stella
PASS /find/{what}(/) #^/find/(?<what>[a-zA-Z0-9\_\-]+)/?$#D /find/cat what = cat
PASS /pay/:when #^/pay/(?<when>[a-zA-Z0-9\_\-]+)$#D /pay/later when = later
Simply replace your regex with this for optional / :
#^/hello/([a-zA-Z0-9-_]+)/?$#

PHP replace wildcards (%s, %d) in string with vars

I have translate function t($var);
function t($word) {
return $this->words[$word];
}
where $this->words is array
$this->words = array(
'word1' => 'word',
'word2' => 'something'
);
I am using functions as <?php echo t('word1'); ?>
and output is : word
My goal is to use wildcards %s, %d, %f to replace them with variables.
Example:
$this->words = array(
'word1' => 'word',
'word2' => 'something',
'sentence' => 'Hello, my name is %s. I am %d years old.'
);
Then parse variables into t() function.
<?php echo t('sentence', array('Mike', 99));
So output will be: Hello, my name is Mike. I am 99 years old.
My Work so far:
function t($word, $vars = array()) {
foreach ($vars as $key) {
if(is_string($key)){
$this->words[$word] = str_replace ('%s', $key, $this->words[$word]);
}
if(is_int($key)) {
$this->words[$word] = str_replace ('%d', $key, $this->words[$word]);
}
if(is_float($key)){
$this->words[$word] = str_replace ('%f', $key, $this->words[$word]);
}
}
return $this->words[$word];
}
But this function doesn't work with more than one of each type of variable.
I've seen people recommend using sprintf but personally I'd recommend using vsprintf: http://www.php.net/manual/en/function.vsprintf.php
function t($word, $vars = array()) {
return vsprintf($this->words[$word], $vars);
}
This allows you to pass in an array of variables instead of passing them in as separate parameters.
Generally translate functions will first check for a translation and if not found just return the lookup key.
function t($word, $vars = array()) {
return isset($this->words[$word]) ? vsprintf($this->words[$word], $vars) : $word;
}
You can define the t function like below :
function t($array)
{
return sprintf($array['sentence'],$array['word1'],$array['word1']);
}
Where array is :
$array = array(
'word1' => 'word',
'word2' => 'something',
'sentence' => 'Hello, my name is %s. I am %d years old.'
);
Call the function :
echo t($array);

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

Categories