Improving the router - php

Right now, I'm trying to improve my MVC router dispatch function. My main problem is that users can attempt to access objects and if it fails, obviously, a blank page is returned.
For example, if I have an object named error and one of the class methods is setType(), a user can enter error/settype is the URI.
All urls are written to index.php?url=$1
$url = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
$controller = !empty($url[0]) ? $url[0] : 'home';
$method = !empty($url[1]) ? $url[1] : 'index';
$params = !empty($url[2]) ? $url[2] : $_POST;
if (class_exists($controller)){
$dispatchedController = new $controller;
if (! method_exists($controller, $method)){
/**
* Error handling
**/
}
return $dispatchedController->$method($params);
} else{
/**
* Error handling
**/
}
How could I improve my router? Providing a whitelist or blacklist isn't very practical, I would think.

Related

PHP MVC - Open a specific Tab using URL Format

I'm using PHP MVC and I've written below code to Create URL & load core controller. My URL Format is like this - "/controller/method/params". Now I'm facing some issue with it if I try opening a specific Nav Tab by passing #id of the tab in the URL, It is ignoring that part. I want if I pass the Id of a tab then it should open that active tab. Any help will be highly appreciated.
<?php
/*
* App Core Class
* Creates URL & loads core controller
* URL FORMAT - /controller/method/params
*/
class Core
{
protected $currentController = 'Pages';
protected $currentMethod= 'index';
protected $params = [];
public function __construct()
{
//print_r($this->getUrl());
$url = $this->getUrl();
if (empty($url) || !in_array("user_images", $url)){
//Look in controller for first value
if (file_exists('../app/controllers/'. ucwords($url[0]) . '.php' )) {
//if exists, set as controller
$this->currentController = ucwords($url[0]);
//Unset 0 Index
unset($url[0]);
}
require_once '../app/controllers/' . $this->currentController . '.php';
//instantiate controller class
$this->currentController = new $this->currentController;
//check for second part of the url
if (isset($url[1])) {
if (method_exists($this->currentController, $url[1])) {
$this->currentMethod = $url[1];
unset($url[1]);
}
}
//echo $this->currentMethod;
// Get Params
$this->params = $url ? array_values($url) : [];
//call a callback with array of params
call_user_func_array([$this->currentController, $this->currentMethod],
$this->params);
}
}
public function getUrl(){
if (isset($_GET['url'])) {
$url = rtrim($_GET['url'],'/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/',$url);
return $url;
}
}
}
What changes I can do in the getUrl method?
The # is only used on the client side, the browser, as an anchor point to an element in the page. Also, it can be used on a JavaScript context to work with tabs etc.
It is not handled on server side, you should just sent parameters: ?tab=id
You can than access the tab parameter in your server side script.

Redirect if route does'n exist

I have a question : so for examples I have an app in symfony3 which have the following routes : /admin/login,admin/news,admin/gallery, but the route /admin/authentification doesn't exist. So the idea is if the route doesn't exist I want to redirect the user to homepage /. Can you help me please ? Thanks in advance and sorry for my english
I'm not confident this is the best solution, but you can use a UrlMatcher to check that the URL you're passing correlates to an available route:
/**
* #Route("/debug")
*/
public function DebugAction(){
$router = $this->get('router');
//Get all the routes that exist.
$routes = $router->getRouteCollection();
$context = $router->getContext();
$urlMatcher = new UrlMatcher($routes, $context);
$url = '/admin/login';
try{
//UrlMatcher::match() will throw a ResourceNotFoundException if the route
//doesn't exist.
$urlMatcher->match($url);
return $this->redirect($url);
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e){
return $this->redirect('/');
}
}
I'm not particularly keen on this solution because it relies on catching an exception, rather than checking a boolean value to determine if the route exists.
You can check if the rout exist.
function routeExists($name)
{
// I assume that you have a link to the container in your twig extension class
$router = $this->container->get('router');
return (null === $router->getRouteCollection()->get($name)) ? false : true;
}
And depending on result, do the redirect to the rout, or to the default webpage, or whatever you need.

PHP MVC Routing > How can I create a custom route?

Im using PHP MVC for my site and I have an issue with routing.
When I go to the index (front page), I use http://www.example.com or http://www.example.com/index.
When I go to the contact page, I use http://www.example.com/contact.
When I go to the services or about pages, I use http://www.example.com/content/page/services or http://www.example.com/content/page/about.
My index and contact pages have their own controllers because they are static pages. But the services and about pages are pulled from my db, thus dynamic. So I created a controller, named it content and just pass the parameters needed to get whatever page I want.
I want to make my URLs more consistent. If I go to the services or about pages, I want to use http://www.example.com/services or http://www.example.com/about.
How can I change my routing to meet this requirement? I ultimately would like to be able to create pages in my db, and then pull the page with a URL that looks like it has its own controller. Instead of having to call the content controller to get it to work.
Below are my controllers and what methods they contain, as well as my routing code.
Controllers:
IndexController
function: index
ContentController
function: page
function: sitemap
ContactController
function: index
function: process
Routing
class Application
{
// #var mixed Instance of the controller
private $controller;
// #var array URL parameters, will be passed to used controller-method
private $parameters = array();
// #var string Just the name of the controller, useful for checks inside the view ("where am I ?")
private $controller_name;
// #var string Just the name of the controller's method, useful for checks inside the view ("where am I ?")
private $action_name;
// Start the application, analyze URL elements, call according controller/method or relocate to fallback location
public function __construct()
{
// Create array with URL parts in $url
$this->splitUrl();
// Check for controller: no controller given ? then make controller = default controller (from config)
if (!$this->controller_name) {
$this->controller_name = Config::get('DEFAULT_CONTROLLER');
}
// Check for action: no action given ? then make action = default action (from config)
if (!$this->action_name OR (strlen($this->action_name) == 0)) {
$this->action_name = Config::get('DEFAULT_ACTION');
}
// Rename controller name to real controller class/file name ("index" to "IndexController")
$this->controller_name = ucwords($this->controller_name) . 'Controller';
// Check if controller exists
if (file_exists(Config::get('PATH_CONTROLLER') . $this->controller_name . '.php')) {
// Load file and create controller
// example: if controller would be "car", then this line would translate into: $this->car = new car();
require Config::get('PATH_CONTROLLER') . $this->controller_name . '.php';
$this->controller = new $this->controller_name();
// Check for method: does such a method exist in the controller?
if (method_exists($this->controller, $this->action_name)) {
if (!empty($this->parameters)) {
// Call the method and pass arguments to it
call_user_func_array(array($this->controller, $this->action_name), $this->parameters);
} else {
// If no parameters are given, just call the method without parameters, like $this->index->index();
$this->controller->{$this->action_name}();
}
} else {
header('location: ' . Config::get('URL') . 'error');
}
} else {
header('location: ' . Config::get('URL') . 'error');
}
}
// Split URL
private function splitUrl()
{
if (Request::get('url')) {
// Split URL
$url = trim(Request::get('url'), '/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/', $url);
// Put URL parts into according properties
$this->controller_name = isset($url[0]) ? $url[0] : null;
$this->action_name = isset($url[1]) ? $url[1] : null;
// Remove controller name and action name from the split URL
unset($url[0], $url[1]);
// rebase array keys and store the URL parameters
$this->parameters = array_values($url);
}
}
}
In order to do this you should map your urls to controllers, check following example:
// route mapping 'route' => 'controller:method'
$routes = array(
'/service' => 'Content:service'
);
also controller can be any php callable function.
Answer Version 2:
Brother in the simplest mode, let's say you have an entity like below:
uri: varchar(255), title: varchar(255), meta_tags: varchar(500), body: text
and have access to StaticPageController from www.example.com/page/ url and what ever it comes after this url will pass to controller as uri parameter
public function StaticPageController($uri){
// this can return a page entity
// that contains what ever a page needs.
$page = $pageRepository->findByUri($uri)
// pass it to view layer
$this->renderView('static_page.phtml', array('page' => $page));
}
I hope this helps.

Friendly URL's with an IndexController

My current router / FrontController is setup to dissect URL's in the format:
http://localhost/controller/method/arg1/arg2/etc...
However, I'm not sure how to get certain requests to default to the IndexController so that I can type:
http://localhost/contact
or
http://localhost/about/portfolio
Instead of:
http://localhost/index/contact
or
http://localhost/index/about/portfolio
How is this accomplished?
<?php
namespace framework;
class FrontController {
const DEFAULT_CONTROLLER = 'framework\controllers\IndexController';
const DEFAULT_METHOD = 'index';
public $controller = self::DEFAULT_CONTROLLER;
public $method = self::DEFAULT_METHOD;
public $params = array();
public $model;
public $view;
function __construct() {
$this->model = new ModelFactory();
$this->view = new View();
}
// route request to the appropriate controller
public function route() {
// get request path
$basePath = trim(substr(PUBLIC_PATH, strlen($_SERVER['DOCUMENT_ROOT'])), '/') . '/';
$path = trim(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH), '/');
if($basePath != '/' && strpos($path, $basePath) === 0) {
$path = substr($path, strlen($basePath));
}
// determine what action to take
#list($controller, $method, $params) = explode('/', $path, 3);
if(isset($controller, $method)) {
$obj = __NAMESPACE__ . '\\controllers\\' . ucfirst(strtolower($controller)) . 'Controller';
$interface = __NAMESPACE__ . '\\controllers\\' . 'InterfaceController';
// make sure a properly implemented controller and corresponding method exists
if(class_exists($obj) && method_exists($obj, $method) && in_array($interface, class_implements($obj))) {
$this->controller = $obj;
$this->method = $method;
if(isset($params)) {
$this->params = explode('/', $params);
}
}
}
// make sure we have the appropriate number of arguments
$args = new \ReflectionMethod($this->controller, $this->method);
$totalArgs = count($this->params);
if($totalArgs >= $args->getNumberOfRequiredParameters() && $totalArgs <= $args->getNumberOfParameters()) {
call_user_func_array(array(new $this->controller, $this->method), $this->params);
} else {
$this->view->load('404');
}
}
}
You can use your URLs by one of two methods:
Establish the controllers the way your routing defines them
example.com/contact => Have a "contact" controller with default or index action
example.com/about/portfolio => Have an "about" controller with a "portfolio" action
Because your currently available routing says your URL is treated like "/controller/method", there is no other way.
Establish dynamic routing to allow multiple URLs to be handled by a single controller
Obviously this needs a bit of configuration because one cannot know which URLs are valid and which one should be redirected to the generic controller, and which ones should not. This is somehow a replacement for any of the rewriting or redirecting solutions, but as it is handled on the PHP level, change might be easier to handle (some webserver configurations do not offer .htaccess because of performance reasons, and it generally is more effort to create these).
Your configuration input is:
The URL you want to be handled and
The controller you want the URL passed to, and it's action.
You'll end up having an array structure like this:
$specialRoutes = array(
"/contact" => "IndexController::indexAction",
"/about/portfolio" => "IndexController::indexAction"
);
What's missing is that this action should get the current URL passed as a parameter, or that the path parts become designated parameters within your URL schema.
All in all this approach is a lot harder to code. To get an idea, try to look at the routing of common MVC frameworks, like Symfony and Zend Framework. They offer highly configurable routing, and because of this, the routing takes place in multiple classes. The main router only reads the configuration and then passes the routing of any URL to the configured routers if a match is detected.
Based on your code snippet I'd do it like this (pseudo php code):
$handler = get_controller($controller);
if(!$handler && ($alias = lookup_alias($path))) {
list($handler, $method) = $alias;
}
if(!$handler) error_404();
function lookup_alias($path) {
foreach(ALL_CONTROLLERS as $controller) {
if(($alias = $controller->get_alias($path))) {
return $alias;
}
}
return null;
}
So basically in case there is no controller to handle a certain location you check if any controller is configured to handle the given path as an alias and if yes return that controller and the method it maps to.
You can create a rewrite in your webserver for these exceptions. For example:
RewriteRule ^contact$ /index/contact
RewriteRule ^about/portfolio$ /about/portfolio
This will allow you to have simplified URLs that map to your regular structure.
You could have a dynamic rule if you are able to precisely define what should be rewritten to /index. For example:
RewriteRule ^([a-z]+)$ /index/$1
Try this dynamic htaccess rewrite rule:
RewriteRule ^(.+)/?$ /index/$1 [QSA]
The QSA flag in the above rule allows you to also add a query string to the end if you want, like this:
http://localhost/contact?arg1=1&arg2=2
EDIT: This rule would also handle cases such as /about/portfolio:
RewriteRule ^(.+)/?(.+)?$ /index/$1 [QSA]

MVC; Arbitrary routing path levels and parameters

I'm working on an (oh no, not another) MVC framework in PHP, primarily for education, but also fun and profit.
Anyways, I'm having some trouble with my Router, specifically routing to the correct paths, with the correct parameters. Right now, I'm looking at a router that (using __autoload()) allows for arbitrarily long routing paths:
"path/to/controller/action"
"also/a/path/to/a/controller/action"
Routing starts at the application directory, and the routing path is essentially parallel with the file system path:
"/framework/application/path/to/controller.class.php" => "action()"
class Path_To_Controller{
public function action(){}
}
"/framework/application/also/a/path/to/a/controller.class.php" => "action()"
class Also_A_Path_To_A_Controller{
public function action(){}
}
This will allow for module configuration files to be available at varying levels of the application file system. The problem is of course, when we introduce routing path parameters, it becomes difficult differentiating where the routing path ends and the path parameters begin:
"path/to/controller/action/key1/param1/key2/param2"
Will obviously be looking for the file:
"/framework/application/path/to/controller/action/key1/param1/key2.class.php"
=> 'param2()'
//no class or method by this name can be found
This ain't good. Now this smells like a design issue of course, but I'm certain there must be a clean way to circumvent this problem.
My initial thoughts were to test each level of the routing path for directory/file existence.
If it hits 1+ directories followed by a file, additional path components are an action followed by parameters.
If it hits 1+ directories and no file is found, 404 it.
However, this is still susceptible to erroneously finding files. Sure this can be alleviated by stricter naming conventions and reserving certain words, but I'd like to avoid that if possible.
I don't know if this is the best approach. Has anyone solved such an issue in an elegant manner?
Well, to answer my own question with my own suggestion:
// split routePath and set base path
$routeParts = explode('/', $routePath);
$classPath = 'Flooid/Application';
do{
// append part to path and check if file exists
$classPath .= '/' . array_shift($routeParts);
if(is_file(FLOOID_PATH_BASE . '/' . $classPath . '.class.php')){
// transform to class name and check if method exists
$className = str_replace('/', '_', $classPath);
if(method_exists($className, $action = array_shift($routeParts))){
// build param key => value array
do{
$routeParams[current($routeParts)] = next($routeParts);
}while(next($routeParts));
// controller instance with params passed to __construct and break
$controller = new $className($routeParams);
break;
}
}
}while(!empty($routeParts));
// if controller exists call action else 404
if(isset($controller)){
$controller->{$action}();
}else{
throw new Flooid_System_ResponseException(404);
}
My autoloader is about as basic as it gets:
function __autoload($className){
require_once FLOOID_PATH_BASE . '/' . str_replace('_', '/', $className) . '.class.php';
}
This works, surprisingly well. I've yet to implement certain checks, like ensuring that the requested controller in fact extends from my Flooid_System_ControllerAbstract, but for the time being, this is what I'm running with.
Regardless, I feel this approach could benefit from critique, if not a full blown overhaul.
I've since revised this approach, though it ultimately performs the same functionality. Instead of instantiating the controller, it passes back the controller class name, method name, and parameter array. The guts of it all are in getVerifiedRouteData(), verifyRouteParts() and createParamArray(). I'm thinking I want to refactor or revamp this class though. I'm looking for insight on where I can optimize readability and usability.:
class Flooid_Core_Router {
protected $_routeTable = array();
public function getRouteTable() {
return !empty($this->_routeTable)
? $this->_routeTable
: null;
}
public function setRouteTable(Array $routeTable, $mergeTables = true) {
$this->_routeTable = $mergeTables
? array_merge($this->_routeTable, $routeTable)
: $routeTable;
return $this;
}
public function getRouteRule($routeFrom) {
return isset($this->_routeTable[$routeFrom])
? $this->_routeTable[$routeFrom]
: null;
}
public function setRouteRule($routeFrom, $routeTo, Array $routeParams = null) {
$this->_routeTable[$routeFrom] = is_null($routeParams)
? $routeTo
: array($routeTo, $routeParams);
return $this;
}
public function unsetRouteRule($routeFrom) {
if(isset($this->_routeTable[$routeFrom])){
unset($this->_routeTable[$routeFrom]);
}
return $this;
}
public function getResolvedRoutePath($routePath, $strict = false) {
// iterate table
foreach($this->_routeTable as $routeFrom => $routeData){
// if advanced rule
if(is_array($routeData)){
// build rule
list($routeTo, $routeParams) = each($routeData);
foreach($routeParams as $paramName => $paramRule){
$routeFrom = str_replace("{{$paramName}}", "(?<{$paramName}>{$paramRule})", $routeFrom);
}
// if !advanced rule
}else{
// set rule
$routeTo = $routeData;
}
// if path matches rule
if(preg_match("#^{$routeFrom}$#Di", $routePath, $paramMatch)){
// check for and iterate rule param matches
if(is_array($paramMatch)){
foreach($paramMatch as $paramKey => $paramValue){
$routeTo = str_replace("{{$paramName}}", $paramValue, $routeTo);
}
}
// return resolved path
return $routeTo;
}
}
// if !strict return original path
return !$strict
? $routePath
: false;
}
public function createParamArray(Array $routeParts) {
$params = array();
if(!empty($routeParts)){
// iterate indexed array, use odd elements as keys
do{
$params[current($routeParts)] = next($routeParts);
}while(next($routeParts));
}
return $params;
}
public function verifyRouteParts($className, $methodName) {
if(!is_subclass_of($className, 'Flooid_Core_Controller_Abstract')){
return false;
}
if(!method_exists($className, $methodName)){
return false;
}
return true;
}
public function getVerfiedRouteData($routePath) {
$classParts = $routeParts = explode('/', $routePath);
// iterate class parts
do{
// get parts
$classPath = 'Flooid/Application/' . implode('/', $classParts);
$className = str_replace('/', '_', $classPath);
$methodName = isset($routeParts[count($classParts)]);
// if verified parts
if(is_file(FLOOID_PATH_BASE . '/' . $classPath . '.class.php') && $this->verifyRouteParts($className, $methodName)){
// return data array on verified
return array(
'className'
=> $className,
'methodName'
=> $methodName,
'params'
=> $this->createParamArray(array_slice($routeParts, count($classParts) + 1)),
);
}
// if !verified parts, slide back class/method/params
$classParts = array_slice($classParts, 0, count($classParts) - 1);
}while(!empty($classParts));
// return false on not verified
return false;
}
}

Categories