match items inside curly braces and map them to a given string - php

I am trying to make a routing system in php which uses regex. What I need is to match the route specified in route collection and check if the url matches with it, and if it does, return the parameters in the route by matching with the url.
For example
$route="user/{username}/id/{id}/age/{age}/profile";
$url="user/joe/id/99/age/33/profile";
First thing is to check if the $url matches the $route pattern, return false if it doesn't.
Then I need to return an array containing
[
'username'=>'joe',
'id'=>'99',
'age'=>'33',
]
I'm not good at this at all, I had a clumsy go at it.
Here's my current code
<?php
$r="user/username/{name}/id/{id}/age/{age}/profile";
$u="user/username/joe/id/99/age/33/profile";
route::match($r, $u);
class route{
public static function match($route, $url)
{
if(strpos($route, '{')===FALSE)
{
if(strcmp($route, $url)==0)
{
return TRUE;
}
return FALSE;
}
$vars=[];
$umatches=[];
preg_match_all('/\{(.*?)\}/', $route, $matches);
preg_match('/(.*?)\{/', $route, $amatches);
preg_match_all('/\}(.*?)\{/', $route, $bmatches);
$a=preg_split(end($bmatches[1]), $route);
$b=preg_split('/\}(.*?)/', $a[1]);
array_push($umatches, $amatches[1]);
foreach ($bmatches[1] as $key => $value)
{
array_push($umatches, $value);
}
array_push($umatches, $b[1]);
$pattern="/".str_replace('/', '\/', $amatches[1])."/";
$split=preg_split($pattern, $url);
$i=0;
foreach ($umatches as $key => $value) {
$value=str_replace('/', '\/', $value);
$value='/'.$value.'/';
$r=preg_split($value, $url);
$url=$r[1];
if($i>0)array_push($vars, $r[0]);
$i++;
}
print_r($vars);
if(sizeof($matches[1])!=sizeof($vars)) return FALSE;
$params=[];
for ($i=0; $i < sizeof($matches[1]); $i++) {
$params[$matches[1][$i]]=$vars[$i];
}
print_r($params);
return $params;
}
}
Here I ran the code http://ideone.com/blljFM

Not a php guru here. So below is just a quick 2-step solution using
pseudo-code.
global $TemplateArray;
global $UrlArray;
function GetRoutParams ( $strUrlTemplate, $strUrl )
{
$TemplateArray = [];
$UrlArray = [];
// Step 1. Create the regex from the template
$strUrlRegex = preg_replace_callback('~\{([^{}]+)\}~',
function( $matches ){
$repl = '([^/]+)';
// push $matches[1] into a $TemplateArray
return $repl;
},
$strUrlTemplate);
// Step 2. Create the hash from the regex
if ( preg_match($strUrlRegex, $strUrl, $matches) )
{
// Peel off the matches
// (Or, make a hash)
for ( int $i = 0; $i < $matches.count; $i++ )
{
push $UrlArray, $TemplateArray[$i];
push $UrlArray, $matches[$i];
}
// (Or, make a hash)
// $UrlHash[ $TemplateArray[$i] ] = $matches[$i];
}
else
{
return false;
}
return true;
}

Check out this regex with global match against your string:
(user|id|age)\/([^\/]+) it matches the keyword, a '/' and the next path element (which must not contain ANY '/'). Also be aware that for example a "superuser/1/" string would also be matched.
Each match should give you a key-value pair. Looping over the matches you can build your arrays/dicts as you intend.
<?php
$r="user/username/{name}/id/{id}/age/{age}/profile";
$u="user/username/joe/id/99/age/33/profile";
route::match($r, $u);
class route{
public static function match($route, $url)
{
preg_match_all('/(username|id|age)\/([^\/]+)/', $url, $matches);
$params=[];
for ($i=0; $i < sizeof($matches[1]); $i++) {
$params[$matches[1][$i]]=$matches[2][$i];
}
print_r($params);
return $params;
}
}
A little bit more elaborate:
<?php
$r=":user/username/[^/]+/id/[^/]+/age/[^/]+/profile:";
$u="user/username/joe/id/99/age/33/profile";
$k="username|id|age"; # valid keys
print_r(route::match($r, $u, $k));
class route{
public static function match($route, $url, $valid_url_keys)
{
$number_of_expected_key_value_pairs = 3;
$number_of_matches = preg_match_all(":($valid_url_keys)/([^/]+):", $url, $matches);
if($number_of_matches == $number_of_expected_key_value_pairs)
{
$params=[];
for ($i=0; $i < sizeof($matches[1]); $i++) {
$params[$matches[1][$i]]=$matches[2][$i];
}
return $params;
}
else {
return FALSE;
}
}
}
Changed regex delimiter to colon, since the slash is a url-pattern, we need less escaping.
Only returns matches if they match the route specification.

Update: I've implemented sln's solution with a few fixes. Here's it
class Route
{
private static $tmp = array();
public static function GetRoutParams($strUrlTemplate, $strUrl)
{
$strUrlRegex = preg_replace_callback('~\{([^{}]+)\}~',
function ($matches)
{
$repl = '([^)]+)';
self::$tmp[] = $matches[1];
return $repl;
}
, $strUrlTemplate);
$UrlArray = array();
$matches = array();
$strUrlRegex = str_replace('/', '\/', $strUrlRegex);
if (preg_match("/^" . $strUrlRegex . "$/", $strUrl, $matches))
{
for ($i = 0; $i < count(self::$tmp); $i++)
{
$UrlArray[self::$tmp[$i]] = $matches[$i + 1];
}
self::$tmp = array();
return $UrlArray;
}
return false;
}
}
I've completed the code; though it feels inefficient. I'd be grateful if someone helps making the code more efficient.
<?php
$route = "user/{name}/id/{id}/age/{age}/ash";
$url = "user/joe/id/99/age/33/ash";
route::match($route, $url);
class route
{
public static function match($route, $url)
{
if (strpos($route, '{') === FALSE)
{
if (strcmp($route, $url) == 0)
{
return TRUE;
}
return FALSE;
}
$vars = [];
$umatches = [];
//get all parameters in curly braces in $route
preg_match_all('/\{(.*?)\}/', $route, $matches);
//get the string before first occurrence of { in $route
preg_match('/(.*?)\{/', $route, $amatches);
//get all strings between } and { in $route
preg_match_all('/\}(.*?)\{/', $route, $bmatches);
//get the string after last }
if (!empty($bmatches[1])){
$a = preg_split(end($bmatches[1]) , $route);
$b = preg_split('/\}(.*?)/', end($a));
}
else{
$a = preg_split('/' . str_replace('/', '\/', end($amatches)) . '/', $route);
$b = preg_split('/\}(.*?)/', end($a));
}
//push the matches into array $umatches
if (!empty($amatches[1])) array_push($umatches, $amatches[1]);
if (!empty($bmatches[1]))
{
foreach($bmatches[1] as $key => $value)
{
array_push($umatches, $value);
}
}
if (!empty($b[1])) array_push($umatches, $b[1]);
//check if the $url matches with $route template
$prev = - 1;
foreach($umatches as $key => $value)
{
$pos = strpos($url, $value);
if ($pos !== FALSE)
{
if ($prev > $pos) return FALSE;
$prev = $pos;
}
else return FALSE;
}
//push the parameter values in $url into $vars array
$i = 0;
foreach($umatches as $key => $value)
{
$value = str_replace('/', '\/', $value);
$value = '/' . $value . '/';
$r = preg_split($value, $url);
$url = $r[1];
if (!empty($r[0])) array_push($vars, $r[0]);
$i++;
}
if (!empty($r[1])) array_push($vars, $r[1]);
//map the values in $url with the parameters in $route template
$params = [];
for ($i = 0; $i < sizeof($matches[1]); $i++)
{
$params[$matches[1][$i]] = $vars[$i];
}
return $params;
}
}
The code now properly returns the parameters. The problems I had was the code was being preg_split by empty strings in some places. Also I didn't push the value if the parameter occurred in the end of string; for example - user/{username}, here {username} wasn't being pushed properly. Also the comparing the template with given url using sizeof() doesn't always give correct result.

Related

how to extract values from url and match them using preg_match

$pathInfo = 'store/view/14342/galaxy-s10-lite-sm-g770-8gb';
I have a route array as -
$route = [
'store/{id}/{pid}' => ['home/store', ['id', 'exuo', 'pid']],
'store/{id}' => ['home/store', ['id']],
'store/view/{id}/{name}' => ['home/store', ['id','name']], // pathInfo should match this route
];
How do I match the the $pathInfo with its corresponding route.
this is how i tried to do it -
public function process_route() {
if (is_array($this->routes()) && count($this->routes()) > 0) {
//print_r([$this->rawPathInfo, $this->request, $this->route]) . "<br>";
foreach ($this->routes() as $pattern => $rules) {
$match = str_replace('/', '\/', $pattern);
if (preg_match("/$match/", $this->pathInfo)) {
if (count(explode('/', $this->pathInfo)) == count(explode('/', $pattern))) {
$this->set_params($rules);
return $rules[0];
}
}
}
}
return FALSE;
}
protected function set_params($rules) {
if (count($rules) >= 2) {
if (is_array($rules[1]) && count($rules) >= 2) {
$pathInfoArray = explode("/", $this->pathInfo);
foreach ($rules[1] as $key) {
$index1 = array_search($key, $pathInfoArray);
$value = (isset($pathInfoArray[$index1 + 1])) ? $pathInfoArray[$index1 + 1] : self::$NOT_FOUND;
if ($value !== self::$NOT_FOUND)
$_GET[$key] = $value;
}
}
}
}
the only diff is here I defined the routes as
$routes =[
'store/id/.*/exuo/.*/pid/.*' => ['home/store', ['id', 'exuo', 'pid']],
];
and was matching the values with the (.*) fields.
You could transform your route paths into appropriate regular expressions, check $pathInfo against each of them, then return the first one which matches (if any):
/**
* #param string[] $routes
*/
function findRoute(array $routes, string $pathInfo): ?string
{
foreach (array_keys($routes) as $routePath) {
$pattern = '~^' . preg_replace('/{.*?}/', '[^/]+', $routePath) . '$~';
if (preg_match($pattern, $pathInfo)) {
return $routePath;
}
}
return null;
}
Usage:
findRoute($routes, $pathInfo);
Demo: https://3v4l.org/DoimK

PHP in_array and String contains

I have any array of data "example.com/imports", "example.com/var", "example.com/js" i want to remove all urls which contain this for sitemap.
Some of my url data is like the following
"example.com/imports/product.html",
"example.com/imports/product1.html",
"example.com/var/cache/5t46fdgdyg7644gfgfdgr",
"example.com/js/scripts.js"
I have this code
for ($i = 0; $i <= count($urls); $i++) {
$url = $urls[$i];
if (in_array($url, $remove_urls)) {
// found remove url
}else{
echo $url;
}
}
However this only removes if the url is exact match such as "example.com/imports" is there a way to check against start
Instead of in_array($url, $remove_urls) try to use strpos:
foreach ($urls as $url) {
$remove = false;
// loop $remove_urls and check if $url starts with any of them
foreach ($remove_urls as $remove_url) {
if (strpos($url, $remove_url) === 0) {
$remove = true;
break;
}
}
if ($remove) {
// remove url
} else {
echo $url;
}
}
You can use preg_grep function like that:
$urls = ['imports', 'var', 'js'];
$url_pattern = '/example.com\/(' . implode('|', $urls) . ')\/.*/';
$removed = preg_grep($url_pattern, $remove_urls);
here an example.

Getting custom replace to work similar to how PHP PDO works

I just want to know how to replace a certain index character with an array constantly like how PDO works in PHP? Here is my code;
The the code
private $string;
public function __construct($string = null) {
if ($string !== null) {
$this->string = $string;
} else {
$this->string = '';
}
}
public function getString() {
return $this->string;
}
public function replaceWith($index, $array = array()) {
$lastArrayPoint = 0;
$i = 0;
while ($i < sizeof($this->string)) {
if (substr($this->string, $i, $i + 1) == $index) {
$newString[$i] = $array[$lastArrayPoint];
$i = $i . sizeof($array[$lastArrayPoint]);
$lastArrayPoint++;
} else {
$newString[$i] = $this->string[$i];
}
$i++;
}
return $this;
}
and the executing code
$string = new CustomString("if ? == true then do ?");
$string->replaceWith('?', array("mango", "print MANGO"));
echo '<li><pre>' . $string->getString() . '</pre></li>';
Thank you for the help I hope I will recieve.
$string = "if %s == true then do %s. Escaping %% is out of the box.";
$string = vsprintf($string, array("mango", "print MANGO"));
echo "<li><pre>$string</pre></li>";
str_replace has an optional count parameter, so you can make it replace one occurrance at a time. You can just loop through the array, and replace the next question mark for element N.
$string = "if %s == true then do %s";
$params = array("mango", "print MANGO");
foreach ($params as $param)
$string = str_replace('?', $param, $string, 1);
Thanks for the help guys but they did not work the way I wanted it to work. I have found a way to get it too work. Here is the code
public function replaceWith($index, $array = array()) {
$arrayPoint = 0;
$i = 0;
$newString = "";
while ($i < strlen($this->string)) {
if (substr($this->string, $i, 1) === $index) {
$newString .= $array[$arrayPoint];
$arrayPoint++;
} else {
$newString .= substr($this->string, $i, 1);
}
$i++;
}
$this->string = $newString;
return $this;
}
if anyone has a better way then you can tell me but for now this works.

Converting string to array address

I have dynamically generated array $array[], that could be multidimensional and I have a function that return string, which contains address in array (existing one). My question is: how to create or convert string $a = 'array[1]' to address $array[1]?
Example:
$array = [1,2,3,4];
$string = 'array[2]';
function magic($array, $string){
//some magic happens
return $result;
$result = magic($array, $string);
echo $result;
// and 3 is displayed;
Is there a function already to do this? Is it possible to do this?
This code is a modification of ResponseBag::get() from the wonderful HttpFoundation project:
function magic($array, $path, $default = null)
{
if (false === $pos = strpos($path, '[')) {
return $array;
}
$value = $array;
$currentKey = null;
for ($i = $pos, $c = strlen($path); $i < $c; $i++) {
$char = $path[$i];
if ('[' === $char) {
if (null !== $currentKey) {
throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "[" at position %d.', $i));
}
$currentKey = '';
} elseif (']' === $char) {
if (null === $currentKey) {
throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "]" at position %d.', $i));
}
if (!is_array($value) || !array_key_exists($currentKey, $value)) {
return $default;
}
$value = $value[$currentKey];
$currentKey = null;
} else {
if (null === $currentKey) {
throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "%s" at position %d.', $char, $i));
}
$currentKey .= $char;
}
}
if (null !== $currentKey) {
throw new \InvalidArgumentException(sprintf('Malformed path. Path must end with "]".'));
}
return $value;
}
echo magic([1,2,3,4], 'array[2]'); // 3
It can be modified to return a reference as well, just sprinkle it with some ampersands :)

Define multiple needles using stripos

How can i define multiple needles and still perform the same actions below. Im trying to define extra keywords such as numbers, numerals, etc... as of now i have to create a duplicate if loop with the minor keyword change.
if (stripos($data, 'digits') !== false) {
$arr = explode('+', $data);
for ($i = 1; $i < count($arr); $i += 2) {
$arr[$i] = preg_replace('/\d/', '', $arr[$i]);
}
$data = implode('+', $arr);
}
Create a function that loops through an array?
function check_matches ($data, $array_of_needles)
{
foreach ($array_of_needles as $needle)
{
if (stripos($data, $needle)!==FALSE)
{
return true;
}
}
return false;
}
if (check_matches($data, $array_of_needles))
{
//do the rest of your stuff
}
--edit added semicolon
function strposa($haystack, $needles=array(), $offset=0) {
$chr = array();
foreach($needles as $needle) {
$res = strpos($haystack, $needle, $offset);
if ($res !== false) $chr[$needle] = $res;
}
if(empty($chr)) return false;
return min($chr);
}
Usage:
$array = array('1','2','3','etc');
if (strposa($data, $array)) {
$arr = explode('+', $data);
for ($i = 1; $i < count($arr); $i += 2) {
$arr[$i] = preg_replace('/\d/', '', $arr[$i]);
}
$data = implode('+', $arr);
} else {
echo 'false';
}
function taken from https://stackoverflow.com/a/9220624/1018682
Though the previous answers are correct, but I'd like to add all the possible combinations, like you can pass the needle as array or string or integer.
To do that, you can use the following snippet.
function strposAll($haystack, $needles){
if(!is_array($needle)) $needles = array($needles); // if the $needle is not an array, then put it in an array
foreach($needles as $needle)
if( strpos($haystack, $needle) !== False ) return true;
return false;
}
You can now use the second parameter as array or string or integer, whatever you want.

Categories