How can I make a router in PHP? - php

I'm currently developing a router for one of my projects and I need to do the following:
For example, imagine we have this array of set routes:
$routes = [
'blog/posts' => 'Path/To/Module/Blog#posts',
'blog/view/{params} => 'Path/To/Module/Blog#view',
'api/blog/create/{params}' => 'Path/To/Module/API/Blog#create'
];
and then if we pass this URL through: http://localhost/blog/posts it will dispatch the blog/posts route - that's fine.
Now, when it comes to the routes that require parameters, all I need is a method of implementing the ability to pass the parameters through, (i.e. http://localhost/blog/posts/param1/param2/param3 and the ability to prepend api to create http://localhost/api/blog/create/ to target API calls but I'm stumped.

Here's something basic, currently routes can have a pattern, and if the application paths start with that pattern then it's a match. The rest of the path is turned into params.
<?php
class Route
{
public $name;
public $pattern;
public $class;
public $method;
public $params;
}
class Router
{
public $routes;
public function __construct(array $routes)
{
$this->routes = $routes;
}
public function resolve($app_path)
{
$matched = false;
foreach($this->routes as $route) {
if(strpos($app_path, $route->pattern) === 0) {
$matched = true;
break;
}
}
if(! $matched) throw new Exception('Could not match route.');
$param_str = str_replace($route->pattern, '', $app_path);
$params = explode('/', trim($param_str, '/'));
$params = array_filter($params);
$match = clone($route);
$match->params = $params;
return $match;
}
}
class Controller
{
public function action()
{
var_dump(func_get_args());
}
}
$route = new Route;
$route->name = 'blog-posts';
$route->pattern = '/blog/posts/';
$route->class = 'Controller';
$route->method = 'action';
$router = new Router(array($route));
$match = $router->resolve('/blog/posts/foo/bar');
// Dispatch
if($match) {
call_user_func_array(array(new $match->class, $match->method), $match->params);
}
Output:
array (size=2)
0 => string 'foo' (length=3)
1 => string 'bar' (length=3)

This is a basic version - simply just a concept version to show functionality and I wouldn't suggest using it in a production environment.
$routes = [
'blog/view' => 'Example#index',
'api/forum/create' => 'other.php'
];
$url = explode('/', $_GET['url']);
if (isset($url[0]))
{
if ($url[0] == 'api')
{
$params = array_slice($url, 3);
$url = array_slice($url, 0, 3);
}
else
{
$params = array_slice($url, 2);
$url = array_slice($url, 0, 2);
}
}
$url = implode('/', $url);
if (array_key_exists($url, $routes))
{
$path = explode('/', $routes[$url]);
unset($path[count($path)]);
$segments = end($path);
$segments = explode('#', $segments);
$controller = $segments[0];
$method = $segments[1];
require_once APP_ROOT . '/app/' . $controller . '.php';
$controller = new $controller;
call_user_func_array([$controller, $method], $params);
}

Related

PHP MVC "route not found"

i am using php mvc and when i try to edit a specific row, it says, "no route matched" but the route is well defined.
Here is the route for match
$router->add('{controller}/{id:\d+}/{action}');
and here is Router.php.
<?php
namespace Core;
/**
*Router
*/
class Router
{
protected $routes = [];
//saves the parameter from the matched route
protected $params = [];
/**
*Below, params=[] indicates that the paramentes in the url can be empty
*/
public function add($route, $params = [])
{
// Convert the route to a regular expression: escape forward slashes
$route = preg_replace('/\//', '\\/', $route);
// Convert variables e.g. {controller}
$route = preg_replace('/\{([a-z]+)\}/', '(?P<\1>[a-z-]+)', $route);
// Convert variables with custom regular expressions e.g. {id:\d+}
$route = preg_replace('/\{([a-z]+):([^\}]+)\}/', '(?P<\1>\2)', $route);
// Add start and end delimiters, and case insensitive flag
$route = '/^' . $route . '$/i';
$this->routes[$route] = $params;
}
/**
*matches the route and sets the parameters
* $url: The route URL
* returns true is match is found else flase
*/
public function match($url)
{
//Matches to the fixed URL format /controller/action
//$reg_exp = "/^(?P<controller>[a-z-]+)\/(?P<action>[a-z-]+)$/";
foreach ($this->routes as $route => $params) {
if (preg_match($route, $url, $matches))
{
//get named capture group values
//$params = [];
foreach ($matches as $key => $match) {
if (is_string($key))
{
$params[$key] = $match;
}
}
$this->params = $params;
return true;
}
}
return false;
}
// foreach ($this->routes as $route => $params) {
// if ($url == $route) {
// $this->params = $params;
// return true;
// }
// }
// return false;
// if (preg_match($reg_exp, $url, $matches))
// {
//
//
// }
//gets the currently matched parameters
public function getParams()
{
return $this->params;
}
public function dispatch($url)
{
$url = $this->removeQueryStringVariables($url);
if($this->match($url)){
$controller = $this->params['controller'];
$controller = $this->convertToStudlyCaps($controller); //Text can easily be converted from uppercase or lowercase to sTudLYcAPs
//$controller = "App\Controllers\\$controller";
$controller = $this->getNameSpace() . $controller;
if (class_exists($controller)){
$controller_object = new $controller($this->params);
$action = $this->params['action'];
$action = $this->convertToCamelCase($action);
if (is_callable([$controller_object, $action]))
{
$controller_object->$action();
}else{
throw new \Exception("Method $action (in controller $controller) not found");
}
}else{
throw new \Exception("Controller class $controller not found");
}
}else {
throw new \Exception("No route matched");
}
}
/**
*Convert the string with hypens to StudlyCaps,
*e.g. post-authors => PostAuthors
*/
protected function convertToStudlyCaps($string)
{
return str_replace(' ', '', ucwords(str_replace('-', ' ', $string)));
}
/**
*Convert the string with hypens to camelCase,
*e.g. add-new => addNew
*/
protected function convertToCamelCase($string)
{
return lcfirst($this->convertToStudlyCaps($string));
}
/**
*This removes query string variable i.e. posts/index&page=1 => posts/index
*
*/
protected function removeQueryStringVariables($url)
{
if($url != '')
{
$parts = explode('&', $url, 2);
if(strpos($parts[0], '=') === false){
$url = $parts[0];
}
else{
$url = '';
}
}
return $url;
}
/*
*Get the namespace for the controller class. The namespace defined in the
*route parameters is added if present.
*/
protected function getNameSpace()
{
$namespace = 'App\Controllers\\';
if(array_key_exists('namespace', $this->params))
{
$namespace .= $this->params['namespace'] . '\\';
}
return $namespace;
}
}
?>
P.S. i can add values to database but cannot edit it, as it says route not matched. if it helps,here is index.php:
<?php
/**
* FIRST CONTROLLER
*
*/
/**
*Twig
*/
require_once dirname(__DIR__) . '/vendor/Twig/lib/Twig/autoload.php';
Twig_Autoloader::register();
/**
*AutoLoader
*
*/
spl_autoload_register(function ($class){
$root = dirname(__DIR__); //gets the parent directory
$file = $root . '/' . str_replace('\\', '/', $class) . '.php';
if (is_readable($file))
{
require $root . '/' . str_replace('\\', '/', $class) . '.php';
}
});
/**
*Error and Exception handling
*
*/
error_reporting(E_ALL);
set_error_handler('Core\Error::errorHandler');
set_exception_handler('Core\Error::exceptionHandler');
/**
*Routing
*
*/
//require '../Core/Router.php';
$router = new Core\Router();
//Routes
$router->add('', ['controller' => 'Home', 'action' => 'index']);
$router->add('{controller}/{action}');
$router->add('admin/{controller}/{action}', ['namespace' => 'Admin']);
$router->add('{controller}/{id:\d+}/{action}');
$router ->dispatch($_SERVER['QUERY_STRING']);
/*
// Display the routing table
echo '<pre>';
//var_dump($router->getRoutes());
echo htmlspecialchars(print_r($router->getRoutes(),true));
echo '<pre>';
//Match the requested route
$url = $_SERVER['QUERY_STRING'];
if ($router->match($url)) {
echo '<pre>';
var_dump($router->getParams());
echo '<pre>';
}else{
echo "No Route Found For URL '$url'";
}
*/
?>

PHP function to get url from route

I have this class:
class Route
{
protected $routes = [
"view_article" => "view/{articleUrl}",
"edit_article" => "edit/{articleId}"
];
}
How can I make a function that returns the url replacing content inside brackets?
For example if I use this code:
$route->getUrl('view_article', 'first-article');
It should return: view/first-article
Then you should make a function that return your variable?
class Route {
function getUrl($find, $replace) {
$entry = isset($this->routes[$find]) ? $this->routes[$find]: false;
if($entry) {
return str_replace(sprintf('{%s}', $find), $replace, $entry);
} else {
return return false;
}
}
}
something like that.
function getUrl($key, $value){
$value = preg_replace('/[\[{\(].*[\]}\)]/U' , $value, $this->routes[$key]);
return $value;
}
A couple of solutions here:
getUrl below will accept the route name as the first parameter, then replace any placeholders with the subsequent variables. Not the best solution - you may have too many or too few variables. When using IDEs, there will be no parameter hinting.
class Route
{
protected $routes = [
"view_article" => "view/{articleUrl}",
"edit_article" => "edit/{articleId}",
"view_page" => "view/{articleId}/{pageName}"
];
public function getUrl() {
$arg_list = func_get_args();
$route = $this->routes[$arg_list[0]];
unset($arg_list[0]);
foreach($arg_list as $arg) {
$route = preg_replace('/{[^\}]+}/', $arg, $route, 1);
}
return $route;
}
}
$route = new Route();
var_dump($route->getUrl('view_page', '17', 'hello_world')); //'view/17/hello_world'
An alternative approach is to use an array of arguments and str_replace the key=>value pairs:
class Route
{
protected $routes = [
"view_article" => "view/{articleUrl}",
"edit_article" => "edit/{articleId}",
"view_page" => "view/{articleId}/{pageName}"
];
public function getUrl($routeName, $args) {
$route = $this->routes[$routeName];
foreach($args as $key => $value) {
$route = str_replace(sprintf('{%s}', $key), $value, $route);
}
return $route;
}
}
$route = new Route();
var_dump($route->getUrl('view_page', ['articleId' => 17, 'pageName' => 'hello_world'])); //'view/17/hello_world'
In both cases, be sure to include additional checks (route exists, all the variables have been replaced, etc).

Dynamic instantation of namespaced methods in php

There are few routers out there but I decided to create a very simple route for a very light site.
Here is my index.php
$route = new Route();
$route->add('/', 'Home');
$route->add('/about', 'About');
$route->add('/contact', 'Contact');
Here is my router:
<?php namespace Laws\Route;
use Laws\Controller\Home;
class Route
{
private $_uri = array();
private $_method = array();
private $_route;
public function __construct()
{
}
public function add($uri, $method = null)
{
$this->_uri[] = '/' . trim($uri, '/');
if ($method != null) {
$this->_method[] = $method;
}
}
public function submit()
{
$uriGetParam = isset($_GET['uri']) ? '/' . $_GET['uri'] : '/';
foreach ($this->_uri as $key => $value) {
if (preg_match("#^$value$#", $uriGetParam)) {
$useMethod = $this->_method[$key];
new $useMethod(); // this returns an error (cannot find Home'
new Home(); // this actually works.
}
}
}
}
new $useMethod(); does not work. returns error 'cannot find Home'
new Home(); actually works.
What am I missing here?
You can use your concurrent way for calling a class or you can use this:
call_user_func(array($classname,$methodname))

zend framework 2 + custom routing

I tried to follow the recommendations from this topic: zend framework 2 + routing database
I have a route class:
namespace Application\Router;
use Zend\Mvc\Router\Http\RouteInterface;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\Mvc\Router\RouteMatch;
class Content implements RouteInterface, ServiceLocatorAwareInterface {
protected $defaults = array();
protected $routerPluginManager = null;
public function __construct(array $defaults = array()) {
$this->defaults = $defaults;
}
public function setServiceLocator(\Zend\ServiceManager\ServiceLocatorInterface $routerPluginManager) {
$this->routerPluginManager = $routerPluginManager;
}
public function getServiceLocator() {
return $this->routerPluginManager;
}
public static function factory($options = array()) {
if ($options instanceof \Traversable) {
$options = ArrayUtils::iteratorToArray($options);
} elseif (!is_array($options)) {
throw new InvalidArgumentException(__METHOD__ . ' expects an array or Traversable set of options');
}
if (!isset($options['defaults'])) {
$options['defaults'] = array();
}
return new static($options['defaults']);
}
public function match(Request $request, $pathOffset = null) {
if (!method_exists($request, 'getUri')) {
return null;
}
$uri = $request->getUri();
$fullPath = $uri->getPath();
$path = substr($fullPath, $pathOffset);
$alias = trim($path, '/');
$options = $this->defaults;
$options = array_merge($options, array(
'path' => $alias
));
return new RouteMatch($options);
}
public function assemble(array $params = array(), array $options = array()) {
if (array_key_exists('path', $params)) {
return '/' . $params['path'];
}
return '/';
}
public function getAssembledParams() {
return array();
}
}
Pay attention that the match() function returns object of the instance of Zend\Mvc\Router\RouteMatch
However in the file Zend\Mvc\Router\Http\TreeRouteStack it checks for object to be the instance of RouteMatch (without prefix of namespace)
if (
($match = $route->match($request, $baseUrlLength, $options)) instanceof RouteMatch
&& ($pathLength === null || $match->getLength() === $pathLength)
)
And the condition fails in my case because of the namespace.
Any suggestions?
Ok, i figured out what the problem was.
Instead of returning Zend\Mvc\Router\RouteMatch I should return Zend\Mvc\Router\Http\RouteMatch
This fixed my problem

Call to a member function on a non-object from another class [duplicate]

This question already has answers here:
Call to a member function on a non-object [duplicate]
(8 answers)
Closed 9 years ago.
I am learning oop and trying to implement php standards PSR-0 & PSR-1 along the way. I have started by creating a small MVC framework based on Swiftlet.
I am trying to call functions from my base View controller within the Controller that I am calling from the url and I get 'Call to a member function set() on a non-object in'.
All the classes load ok. So in my controller I call $this->view->set('helloWorld', 'Hello world!'); but I get an error. I had some trouble trying to get the namespace structure right so maybe this is the cause?
Here is the file structure:
index.php
lib/bootstrap.php
lib/view.php
lib/controller.php
app/controllers/index.php
and here is the code for each:
index.php
<?php
namespace MVC;
// Bootstrap the application
require 'lib/Bootstrap.php';
$app = new lib\Bootstrap;
spl_autoload_register(array($app, 'autoload'));
$app->run();
$app->serve();
bootstrap.php
namespace MVC\lib;
class Bootstrap
{
protected
$action = 'index',
$controller,
$hooks = array(),
$view
;
/**
* Run the application
*/
function run()
{
... Code that gets controller and the action form the url
$this->view = new \lib\View($this, strtolower($controllerName));
// Instantiate the controller
$controllerName = 'app\Controllers\\' . basename($controllerName);
$this->controller = new $controllerName();
// Call the controller action
$this->registerHook('actionBefore');
if ( method_exists($this->controller, $this->action) ) {
$method = new \ReflectionMethod($this->controller, $this->action);
if ( $method->isPublic() && !$method->isFinal() && !$method->isConstructor() ) {
$this->controller->{$this->action}();
} else {
$this->controller->notImplemented();
}
} else {
$this->controller->notImplemented();
}
return array($this->view, $this->controller);
}
<?php
namespace MVC\lib;
class Bootstrap
{
protected
$action = 'index',
$args = array(),
$config = array(),
$controller,
$hooks = array(),
$plugins = array(),
$rootPath = '/',
$singletons = array(),
$view
;
/**
* Run the application
*/
function run()
{
// Determine the client-side path to root
if ( !empty($_SERVER['REQUEST_URI']) ) {
$this->rootPath = preg_replace('/(index\.php)?(\?.*)?$/', '', $_SERVER['REQUEST_URI']);
if ( !empty($_GET['route']) ) {
$this->rootPath = preg_replace('/' . preg_quote($_GET['route'], '/') . '$/', '', $this->rootPath);
}
}
// Extract controller name, view name, action name and arguments from URL
$controllerName = 'Index';
if ( !empty($_GET['route']) ) {
$this->args = explode('/', $_GET['route']);
if ( $this->args ) {
$controllerName = str_replace(' ', '/', ucwords(str_replace('_', ' ', str_replace('-', '', array_shift($this->args)))));
}
if ( $action = $this->args ? array_shift($this->args) : '' ) {
$this->action = str_replace('-', '', $action);
}
}
if ( !is_file('app/Controllers/'. $controllerName . '.php') ) {
$controllerName = 'Error404';
}
$this->view = new \lib\View($this, strtolower($controllerName));
// Instantiate the controller
$controllerName = 'app\Controllers\\' . basename($controllerName);
$this->controller = new $controllerName();
// Call the controller action
$this->registerHook('actionBefore');
if ( method_exists($this->controller, $this->action) ) {
$method = new \ReflectionMethod($this->controller, $this->action);
if ( $method->isPublic() && !$method->isFinal() && !$method->isConstructor() ) {
$this->controller->{$this->action}();
} else {
$this->controller->notImplemented();
}
} else {
$this->controller->notImplemented();
}
$this->registerHook('actionAfter');
return array($this->view, $this->controller);
}
<?php
namespace MVC\lib;
class Bootstrap
{
protected
$action = 'index',
$args = array(),
$config = array(),
$controller,
$hooks = array(),
$plugins = array(),
$rootPath = '/',
$singletons = array(),
$view
;
/**
* Run the application
*/
function run()
{
// Determine the client-side path to root
if ( !empty($_SERVER['REQUEST_URI']) ) {
$this->rootPath = preg_replace('/(index\.php)?(\?.*)?$/', '', $_SERVER['REQUEST_URI']);
if ( !empty($_GET['route']) ) {
$this->rootPath = preg_replace('/' . preg_quote($_GET['route'], '/') . '$/', '', $this->rootPath);
}
}
// Extract controller name, view name, action name and arguments from URL
$controllerName = 'Index';
if ( !empty($_GET['route']) ) {
$this->args = explode('/', $_GET['route']);
if ( $this->args ) {
$controllerName = str_replace(' ', '/', ucwords(str_replace('_', ' ', str_replace('-', '', array_shift($this->args)))));
}
if ( $action = $this->args ? array_shift($this->args) : '' ) {
$this->action = str_replace('-', '', $action);
}
}
if ( !is_file('app/Controllers/'. $controllerName . '.php') ) {
$controllerName = 'Error404';
}
$this->view = new \lib\View($this, strtolower($controllerName));
// Instantiate the controller
$controllerName = 'app\Controllers\\' . basename($controllerName);
$this->controller = new $controllerName();
// Call the controller action
$this->registerHook('actionBefore');
if ( method_exists($this->controller, $this->action) ) {
$method = new \ReflectionMethod($this->controller, $this->action);
if ( $method->isPublic() && !$method->isFinal() && !$method->isConstructor() ) {
$this->controller->{$this->action}();
} else {
$this->controller->notImplemented();
}
} else {
$this->controller->notImplemented();
}
$this->registerHook('actionAfter');
return array($this->view, $this->controller);
}
lib/view.php
namespace lib;
class View
{
protected
$app,
$variables = array()
;
public
$name
;
/**
* Constructor
* #param object $app
* #param string $name
*/
public function __construct($app, $name)
{
$this->app = $app;
$this->name = $name;
}
/**
* Set a view variable
* #param string $variable
* #param mixed $value
*/
public function set($variable, $value = null)
{
$this->variables[$variable] = $value;
}
and finally app/controllers/index.php
namespace app\Controllers;
class index extends \lib\Controller
{
public function test()
{
// This gets the error
$this->view->set('helloWorld', 'Hello world!');
}
}
If that is all of the code in your controller, then $this->view is not an object.
Try running the following code:
namespace app\Controllers;
class index extends \lib\Controller
{
public function test()
{
var_dump( $this->view );
exit;
$this->view->set('helloWorld', 'Hello world!');
}
}
You also should know, that in PHP, the __construct() methods are not inherited.
I must have suffered some brain injury.
Oh .. and I fail to see, why this question has mvc tag. While you are trying to write MVC-like thing, the issue itself has no relation to MVC as architectural pattern.

Categories