everyone.
I have created very basic router in PHP and now I am stuck.
The user can navigate to different URLs and pass parameters that can be used to display data for example to get data from an array.
However I am stuck, I do not know how to pass these url parameters so they can be used inside a file.
For example this route
"/user/:id" -> If user navigates to /user/1 -> This executes a callback function and he receives data from an array.
However when the url doesn't have callback function but has a name of a file, the router will load a file, for example the user page.
Router::get("/user/:username", "user.php");
So my question is How can I get the "username" from the route and pass it into the user.php file ?
I have tried using $_GET['username'], however that doesn't work as the url doesn't have ? inside of it.
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']) {
$routeMatch = true;
// 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']);
}
}
}
if(!$routeMatch){
self::notFound();
}
}
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/:username", "user.php");
Router::get("/user/profile/:id", "admin.php");
Router::resolve();
?>
You could pass $matches to the render() method as second optional parameter, and that's it. As well as these variables are accessible in the method scope, they are accessible in all the files included/required from this scope. I.e.:
self::render($route['callback'], $matches);
and in the included file:
print_r($matches);
UPD: In order to IDE not highlighting "unknown" variable, you can add a phpdoc-block somewhere in the included file, like this:
/** #var array $matches */
Related
I am trying to figure out how to achieve a specific URL structure in a Laravel 8 project and the necessary route to achieve this. What I want is:
// Example urls to listings in the business directory.
// These urls should be routed to the directory controller.
www.domain-name.com/example-business-name-d1.html
www.domain-name.com/example-business-name-d15.html
www.domain-name.com/example-business-name-d100.html
www.domain-name.com/example-business-name-d123.html
www.domain-name.com/example-business-name-d432.html
// Example urls to articles/posts in the blog.
// These urls should be routed to the posts controller.
www.domain-name.com/example-post-name-p5.html
www.domain-name.com/example-post-name-p11.html
www.domain-name.com/example-post-name-p120.html
www.domain-name.com/example-post-name-p290.html
www.domain-name.com/example-post-name-p747.html
// We want to avoid the more traditional:
www.domain-name.com/directory/example-business-name-1.html
www.domain-name.com/blog/example-post-name-5.html
This is because we don't want the strings “directory” or “blog” contained in the url for every listing or blog post. Search engine results work better without it.
So far I am using a catch-all route {any} at the bottom of the web.php routes file to “catch all” routes that get that far. I then manipulate the string provided by the path to get the ID and single character token from the end of the urls. I then have these 2 variables but can figure out how to pass these onto the right controllers!
Or am I being really dumb and there is a much better way of achieving this?
Route::get('{any}', function($any = null){
// Break up the url into seperate parts.
$pieces = explode("-", $any);
$pieces = array_reverse($pieces);
$piece = $pieces[0];
// Remove the .html
$piece = substr($piece, 0, -5);
// Get the two parts of the identifier.
$id = substr($piece, 1);
$token = substr($piece, 0, 1);
// Call correct controller based on the token.
switch ($token) {
case "d":
// HERE I WANT TO PASS THE ID ON TO THE SHOW ACTION OF THE DIRECTORY CONTROLLER
break;
case "p":
// HERE I WANT TO PASS THE ID ON TO THE SHOW ACTION OF THE POSTS CONTROLLER
break;
default:
return abort(404);
break;
}
});
I would split the path into 2 variables ($slug and $id) and directly pass it to the controller.
Route::get('{slug}-d{id}.html', 'DirectoryController#show')
->where(['slug' => '([a-z\-]+)', 'id' => '(\d+)']);
Route::get('{slug}-p{id}.html', 'PostController#show')
->where(['slug' => '([a-z\-]+)', 'id' => '(\d+)']);
And in your controllers
class DirectoryController
{
public function show(string $slug, int $id) {}
}
class PostController
{
public function show(string $slug, int $id) {}
}
I can see two ways of achieving this result:
Create an intermediate controller
Route::get('{path}', 'CheckPathController#redirect')
Then in your CheckPathController you do all the checks and your call the proper controller action:
public function redirect(Request $request, $path) {
// Your checks on $path, extract $id and content type
if($isPost) {
$controller = resolve(PostController::class);
return $controller->show($request, $id);
}
if($isBusiness) {
$controller = resolve(BusinessController::class);
return $controller->show($request, $id);
}
// No matches, error 404
abort(404);
}
Complex regex
see: https://laravel.com/docs/8.x/routing#parameters-regular-expression-constraints
I'm not a regexp master, this should be a basic was to match any {word}-{word}-...-p{id}.html pattern but it will break in case of unexpected chars
Route::get('{path}', 'PostController::show')
->where(['path' => '([\w]*-)*p[0-9]+\.html$']);
Route::get('{path}', 'BusinessController::show')
->where(['path' => '([\w]*-)*d[0-9]+\.html$']);
Note that in this case, you controller will receive the pull $path string, so you will need to extract the id there.
You can match the slug using regex
Route::get('/{any}', 'YourController#methodName')->where(['any' => '.*(-d(.*?)\.).*']);
Repeated with p
Then when you pickup your $site in your controller method you can use regex to grab the site.
public function methodName($site)
{
preg_match('/.*(-(d(.*?))\.).*/', $site, $parts); //or something similar, $parts[2] will have what you want
}
OR
This will give your controller method d{number} or p{number}
Route::get('/{site}', function($site) {
$code = preg_match('/.*(-(d(.*?)|p(.*?))\.).*/', $site, $parts) ? $parts[2] : null;
$controllerName = 'ControllerA';
if(isset($code) && !is_null($code) && Str::contains($code, 'p')) {
$controllerName = 'ControllerB';
}
$controller = app()->make('App\Http\Controllers\Application\\' . $controllerName);
return $controller->callAction('methodName', $params = ['code' => $code]);
})->where(['site' => '.*(-(d|p)(.*?)\.).*']);
I am newly trying out TDD with laravel, and I want to assert if a redirect took a user to a url that has an integer param.
I wonder if I could use regex to catch all positive integers.
I'm running this app with the laravel 5.8 framework and I know that the url parameter is 1 because I refresh the database each for each test, so setting the redirect url as /projects/1 works but this sort of hardcoding feels weird.
I've attached a block of code I tried using regex for but this doesn't work
/** #test */
public function a_user_can_create_projects()
{
// $this->withoutExceptionHandling();
//If i am logged in
$this->signIn(); // A helper fxn in the model
//If i hit the create url, i get a page there
$this->get('/projects/create')->assertStatus(200);
// Assumming the form is ready, if i get the form data
$attributes = [
'title' => $this->faker->sentence,
'description' => $this->faker->paragraph
];
//If we submit the form data, check that we get redirected to the projects path
//$this->post('/projects', $attributes)->assertRedirect('/projects/1');// Currently working
$this->post('/projects', $attributes)->assertRedirect('/^projects/\d+');
// check that the database has the data we just submitted
$this->assertDatabaseHas('projects', $attributes);
// Check that we the title of the project gets rendered on the projects page
$this->get('/projects')->assertSee($attributes['title']);
}
I expected the test to treat the argument in assertRedirect('/^projects/\d+'); as regex and then pass for any url like /projects/1 so far it ends in a number, but it takes it as a raw string and expects a url of /^projects/\d+
I'd appreciate any help.
After watching a tutorial by Jeffery Way, he talked about handling this issue.
Here's how he solves the situation
//If we submit the form data,
$response = $this->post('/projects', $attributes);
//Get the project we just created
$project = \App\Project::where($attributes)->first();
// Check that we get redirected to the project's path
$response->assertRedirect('/projects/'.$project->id);
This is not possible by now. You need to test the Location header in the response with a regular expression.
This is a problem because you cann't use the current route name. That's why I did two functions that bring a little bit of readability to your test. You will use this function like this:
// This will redirect to some route with an numeric ID in the URL.
$response = $this->post(route('groups.create'), [...]);
$this->assertResponseRedirectTo(
$response,
$this->prepareRoute('group.detail', '[0-9]+'),
);
This is the implementation.
/**
* Assert whether the response is redirecting to a given URI that match the pattern.
*/
public function assertResponseRedirectTo(Illuminate\Testing\TestResponse\TestResponse $response, string $url): void
{
$lastOne = $this->oldURL ?: $url;
$this->oldURL = null;
$newLocation = $response->headers->get('Location');
$this->assertEquals(
1,
preg_match($url, $newLocation),
sprintf('Should redirect to %s, but got: %s', $lastOne, $newLocation),
);
}
/**
* Build the pattern that match the given URL.
*
* #param mixed $params
*/
public function prepareRoute(string $name, $params): string
{
if (! is_array($params)) {
$params = [$params];
}
$prefix = 'lovephp';
$rep = sprintf('%s$&%s', $prefix, $prefix);
$valuesToReplace = [];
foreach ($params as $index => $param) {
$valuesToReplace[$index] = str_replace('$&', $index . '', $rep);
}
$url = preg_quote(route($name, $valuesToReplace), '/');
$this->oldURL = route($name, $params);
foreach ($params as $index => $param) {
$url = str_replace(
sprintf('%s%s%s', $prefix, $index, $prefix),
$param,
$url,
);
}
return sprintf('/%s/', $url);
}
Have a problem to get the id from the URL in a variable!
The Url is like this domain.com/article/1123/
and its like dynamic with many id's
I want to save the 1123 in a variable please help!
a tried it with this
if(isset($_GET['id']) && !preg_match('/[0-9]{4}[a-zA-Z]{0,2}/', $_GET['id'], $id)) {
require_once('404.php');
} else {
$id = $_GET['id'];
}
The absolute simplest way to accomplish this, is with basename()
echo basename('domain.com/article/1123');
Which will print
1123
the reference url click hear
I would do in this way:
Explode the string using /.
Get the length of the exploded array.
Get the last element, which will be the ID.
Code
$url = $_SERVER[REQUEST_URI];
$url = explode("/", $url);
$id = $url[count($url) - 1];
You should definitely be using parse_url to select the correct portion of the URL – just in case a ?query or #fragment exists on the URL
$parts = explode('/', parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
$parts[0]; // 'domain.com'
$parts[1]; // 'article'
$parts[2]; // '1123'
You'll probably want to reference these as names too. You can do that elegantly with array_combine
$params = array_combine(['domain', 'resource', 'id'], $parts);
$params['domain']; // 'domain.com'
$params['resource']; // 'article'
$params['id']; // '1123'
I'm really feeling like a procrastinator right now so I made you a little router. You don't have to bother dissecting this too much right now; first learn how to just use it, then you can pick it apart later.
function makeRouter ($routes, callable $else) {
return function ($url) use ($routes, $else) {
foreach ($routes as $route => $handler) {
if (preg_match(makeRouteMatcher($route), $url, $values)) {
call_user_func_array($handler, array_slice($values, 1));
return;
}
}
call_user_func($else, $url);
};
}
function makeRouteMatcher ($route) {
return sprintf('#^%s$#', preg_replace('#:([^/]+)#', '([^/]+)', $route));
}
function route404 ($url) {
echo "No matching route: $url";
}
OK, so here we'll define our routes and what's supposed to happen on each route
// create a router instance with your route patterns and handlers
$router = makeRouter([
'/:domain/:resource/:id' => function ($domain, $resource, $id) {
echo "domain:$domain, resource:$resource, id:$id", PHP_EOL;
},
'/public/:filename' => function ($filename) {
echo "serving up file: $filename", PHP_EOL;
},
'/' => function () {
echo "root url!", PHP_EOL;
}
], 'route404');
Now let's see it do our bidding ...
$router('/domain.com/article/1123');
// domain:domain.com, resource:article, id:1123
$router('/public/cats.jpg');
// serving up file: cats.jpg
$router('/');
// root url!
$router('what?');
// No matching route: what?
Yeah, I was really that bored with my current work task ...
That can be done quite simple. First of all, you should create a variable with a string that contains your URL. That can be done with the $_SERVER array. This contains information about your server, also the URL you're actually at.
Second point is to split the URL. This can be done by different ways, I like to use the p_reg function to split it. In your case, you want to split after every / because this way you'll have an array with every single "directory" of your URL.
After that, its simply choosing the right position in the array.
$path = $_SERVER['REQUEST_URI']; // /article/1123/
$folders = preg_split('/', $path); // splits folders in array
$your_id = $folders[1];
To be thorough, you'll want to start with parse_url().
$parts=parse_url("domain.com/article/1123/");
That will give you an array with a handful of keys. The one you are looking for is path.
Split the path on / and take the last one.
$path_parts=explode('/', $parts['path']);
Your ID is now in $path_parts[count($path_parts)-1];
I am trying to create an API request handler that can read wildcards in a string. The ideal situation is something like this.
$myClass->httpGet('/account/[account_id]/list-prefs', function ($account_id) {
// Do something with $account_id
});
Where [account_id] is the wild card. The actual URI would look like:
http://api.example.com/account/123456/list-prefs
The actual function looks like...
function httpGet($resource, $callback) {
$URI = urldecode(str_replace('/'.$this->API_VERSION, '', $_SERVER['REQUEST_URI']));
$match = preg_match_all('/\[([a-zA-Z0-9_]+)\]/', $resource, $array);
if ($resource /*matches with wildcards*/ $URI) {
// Do something with it.
}
...
}
My problem is...
I cannot figure out how to match the string within the function with the URI in order to call the callback.
How to parse the string with the values supplied in the URI (replace [account_id] with 123456).
I think you are missing something like:
tokens = array('[account_id]' => '/\[([a-zA-Z0-9_]+)\]/');
Then:
function replaceTokens($resource) {
# get uri with tokens replaced for actual regular expressions and return it
}
function httpGet($resource, $callback) {
$URI = urldecode(str_replace('/'.$this->API_VERSION, '', $_SERVER['REQUEST_URI']));
$uriRegex = replaceTokens($resource);
$match = preg_match_all($uriRegex, $URI, $array);
if ($match) {
// Do something with it.
}
}
I am trying to build a router function to properly match incoming URI's and match them to an array of stored system URI's. I also have wildcards '(:any)' and '(:num)' similar to CodeIgniter.
Basically, I am trying to get the 'admin/stats/(:num)' entry to match on both 'admin/stats' and admin/stats/1'.
While the script is starting I grab all paths from a separate array and use a foreach to save each path:
route('admin/stats/(:num)', array('#title' => 'Statistics',...));
The function is:
function route($path = NULL, $options = NULL) {
static $routes;
//If no arguments are supplied, return all routes stored.
if(!isset($path) && !isset($options)) {
return $routes;
}
//return options for path if $path is set.
if(isset($path) && !isset($options)) {
//If we have an exact match, return it.
if(array_key_exists($path, $routes)) {
return $routes[$path];
}
//Else, we need to use RegEx to find the correct route options.
else {
$regex = str_replace('/', '\/', $path);
$regex = '#^' . $regex . '\/?$#';
//I am trying to get the array key for $route[$path], but it isn't working.
// route_replace('admin/stats/(:num)') = 'admin/stats/([0-9]+)'.
$uri_path = route_replace(key($routes[$path])); //route_replace replaces wildcards for regex.
if(preg_match($regex, $uri_path)) {
return $routes[$path];
}
}
}
$routes[$path] = $options;
return $routes;
}
Route replace function:
function route_replace($path) {
return str_replace(':any', '.+', str_replace(':num', '[0-9]+', $path));
}
A key/value pair in the $routes array looks like:
[admin/stats/(:num)] => Array
(
[#title] => Statistics //Page title
[#access] => user_access //function to check if user is authorized
[#content] => html_stats //function that returns HTML for the page
[#form_submit] => form_stats //Function to handle POST submits.
)
Thanks for the help. This is my first router and I am not that familiar in making proper Regex's.
'admin/stats/(:num)' will never match 'admin/stats' as in your "pattern" the slash is required. In pseduo-regex you need to do something like 'admin/stats(/:num)'.
There does also seem to be a few bugs in your code. This line
$uri_path = route_replace(key($routes[$path]));
is in the block that is executed when $path is not a key that exists in $routes.
I've tried to rewrite it and this seems to work (this is just the else clause):
foreach( array_keys( $routes ) as $route ) {
$regex = '#^' . $route . '?$#';
//I am trying to get the array key for $route'$path', but it isn't working.
// route_replace('admin/stats/(:num)') = 'admin/stats/('0-9'+)'.
$uri_path = route_replace($regex); //route_replace replaces wildcards for regex.
if(preg_match($uri_path,$path)) {
return $routes[$route];
}
}
But this requires 'admin/stats/(:num)' to be 'admin/stats(/:num)'.
btw if you don't have one already, you should get a debugger (Zend and xDebug are two of the most common ones for PHP). They can be invaluable in solving problems like this.
Also, ask yourself if you need to write a router, or whether you can't just use one of the perfectly good ones out there already...