I have been trying to create a route that accepts parameters to get a single user but I am struggling to understand what I am doing wrong, I am stuck.
Here are the routes: The 1st one works without any issues:
<?php
$router->get('users', 'UsersController@index');
$router->get('users/about', 'UsersController@test');
$router->get('users/:id', 'UsersController@show');
Here is my Router Class, I am matching the urls and using preg_replace so I can get the id dynamically
<?php
namespace App\Core;
class Router
{
/**
* All registered routes.
*
* @var array
*/
public $routes = [
'GET' => [],
'POST' => []
];
/**
* Load a user's routes file.
*
* @param string $file
*/
public static function load($file)
{
$router = new static;
require $file;
return $router;
}
/**
* Register a GET route.
*
* @param string $uri
* @param string $controller
*/
public function get($uri, $controller)
{
$this->routes['GET'][$uri] = $controller;
}
/**
* Register a POST route.
*
* @param string $uri
* @param string $controller
*/
public function post($uri, $controller)
{
$this->routes['POST'][$uri] = $controller;
}
/**
* Load the requested URI's associated controller method.
*
* @param string $uri
* @param string $requestType
*/
public function direct($uri, $requestType)
{
$matches = [];
foreach ($this->routes[$requestType] as $regex => $controller) {
$pattern = "@^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($regex)) . "$@D";
if ( preg_match($pattern, $uri, $matches ) ) {
print_r($matches[0]);
return $this->callAction(
...explode('@', $this->routes[$requestType][$uri])
);
}
}
throw new Exception('No route defined for this URI.');
}
/**
* Load and call the relevant controller action.
*
* @param string $controller
* @param string $action
*/
protected function callAction($controller, $action)
{
$controller = "App\\Controllers\\{$controller}";
$controller = new $controller;
if (! method_exists($controller, $action)) {
throw new Exception(
"{$controller} does not respond to the {$action} action."
);
}
return $controller->$action();
}
}
And in my users controller I simply have a function that gets the id and shows me the user based on $id
/**
* Show selected user.
*/
public function show($id)
{
$id = array_slice(explode('/', rtrim($_SERVER['REQUEST_URI'], '/')), -1)[0];
$user = App::get('database')->get('users', [
'id' => $id
]);
return view('user', compact('user'));
}
If you guys need more infos I can add the whole code into a code-pen. Thanks
In this section ( method direct
)
explode('@', $this->routes[$requestType][$uri])
This should be
explode('@', $this->routes[$requestType][$regex])
Or simply (and preferred):
explode('@', $controller)
as the URI (for the 3rd one) is something like this:
users/10
users/20
And the actual key is: users/:id
which is also the $regex
value (obviously)
Code (For testing only):
$routes = [
'GET' => [
'users'=>'UsersController@index',
'users/about'=>'UsersController@test',
'users/:id'=>'UsersController@show'
],
'POST' => []
];
$requestType = 'GET';
$uri = 'users/10';
foreach ($routes[$requestType] as $regex => $controller) {
$pattern = "@^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($regex)) . "$@D";
if ( preg_match($pattern, $uri, $matches ) ) {
print_r($matches[0]);
echo "\n";
print_r($routes[$requestType][$uri]);
echo "\n";
print_r($routes[$requestType][$regex]);
}
}
Output:
#$matches[0]
users/10
#with $uri as the key - $routes[$requestType][$uri]
<b>Notice</b>: Undefined index: users/10 in <b>[...][...]</b> on line <b>27</b><br />
#with $regex as the key - $routes[$requestType][$regex]
UsersController@show
Also I imagine the first and second one should work, only the one with the actual regex as the key would be affected due to it's "dynamic" nature.
One thing you are missing is arguments from the url, take the 3rd example (users/10
) how do you pass that ID (10
) into your controller? Also, If it was me, I would break the dependency you have on this line $controller = "App\\Controllers\\{$controller}";
as it limits you to only using classes of the App\\Controllers\...
namespace.
So to fix that change your data structure to remove that @
sign. So instead of this:
$router->get('users', 'UsersController@index');
Do it this way:
#Obj::class returns the fully qualified class name (includes namespace)
# PHP 5.6+ I think?
$router->get('users', [UsersController::class,'index']);
Which will actually simplify your code and give you the possibility of doing things like this (simpler and more flexible):
$router->get('users', function(){
//do something simple
});
#or
$router->get('users', 'somefunction');
#or (drop in plugins outside of your normal controller folder)
$router->get('users', 'Plugins/Users/Controllers/User);
So we have to make this slight modifications:
public function direct($uri, $requestType)
{
$matches = [];
foreach ($this->routes[$requestType] as $regex => $controller) {
$pattern = "@^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($regex)) . "$@D";
if ( preg_match($pattern, $uri, $matches ) ) {
//Simplify the code here and also pass the uri as an array
return $this->callAction($controller, explode('/', $uri));
}
}
throw new Exception('No route defined for this URI.');
}
protected function callAction($controller, array $args=[])
{
//you can check types here but all callables work with call_user_func & call_user_func_array
//you may be able to just check !is_callable($controller) for them all if you don't need the granularity
if(is_array($controller)){
//[object, method]
//[class name, method]
if(!class_exists($controller[0]) || !method_exists($controller[0], $controller[1])){
//the router has a direct interface to the end user
//because of this it must handle requests to bad URLs and such
//direct to 404 page, for example something like this
//you can and should "log" the errors, but don't show them
// ---- return $this->error404();
}
}else if(is_object($controller) && !is_callable($controller)){
//closure or magic method __invoke
// ---- return $this->error404();
}else if( !function_exists($controller) ){
//standard functions
// ---- return $this->error404();
}
return call_user_func_array($action, $args);
}
With this simple setup all the args are passed including the name of the controller if it's part of the url. For exampl, using the third route with a value of this users/10
would call
$UsersController->show('users', '10');
It may prove challenging to remove that without baking the "method" into route path: For example
$router->get('users/about', 'UsersController@test');
There is no way to "know" if "users" is important to the "test" method. Now if they matched:
$router->get('test/about', 'UsersController@test');
You could remove it. Typically I have seen this pattern in urls
www.yoursite.com/controller/method/...args
Which gives us a sort of "grantee" as to what the parts are. But it's your code, you may just decide you can discard the first one no matter what...
I should mention I didn't test any of the above code, but based on my experiance these are features you will probably want at some point.
Cheers!