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.
Related
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.
-Myproject
-application
-controllers
-subfolder1
-subfolder2
-subfolder3
-subfolder(..n)
Controller.php
And need to set routes.php
$route['default_controller'] = 'subfolder1/subfolder2/subfolder3/subfolder(..n)/controller';
According to the examples in the docs it does not appear that you can put a Controller more than one level deep inside a subdirectory.
example.com/index.php/subdirectory/controller/function
I also don't think your route looks correct. You would not have home/ at the start of the route unless "home" is a controller or subdirectory name. See examples here.
$route['default_controller'] = 'subdirectory/controller';
I have researched and found in codeigniter 3 system/core/Router.php library file's method _set_default_controller() can not support default controller in subfolder. So i have overrided/ customized this method _set_default_controller() and now it supports n level subfolders and working fine for me.
I have created application/core/MY_Router.php with the below code to override this method _set_default_controller()
<?php
/**
* Override Set default controller
*
* #author : amit
*
* #return void
*/
protected function _set_default_controller()
{
if (empty($this->default_controller))
{
show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.');
}
// Is the method being specified?
$x = explode('/', $this->default_controller);
$dir = APPPATH.'controllers'; // set the controllers directrory path
$dir_arr = array();
foreach($x as $key => $val){
if(!is_dir($dir.'/'.$val)){
// find out class i.e. controller
if(file_exists($dir.'/'.ucfirst($val).'.php')){
$class = $val;
if(array_key_exists(($key+1), $x)){
$method = $x[$key+1]; // find out method i.e. action
}else{
$method = 'index'; // default method i.e. action
}
}else{
// show error message if the specified controller not found
show_error('Not found specified default controller : '. $this->default_controller);
}
break;
}
$dir_arr[] = $val;
$dir = $dir.'/'.$val;
}
//set directory
$this->set_directory(implode('/', $dir_arr));
$this->set_class($class);
$this->set_method($method);
// Assign routed segments, index starting from 1
$this->uri->rsegments = array(
1 => $class,
2 => $method
);
log_message('debug', 'No URI present. Default controller set.');
}
?>
Now we can set default controller in application/config/routes.php like below
// without action method name
$route['default_controller'] = 'subfoler1/subfoler2/subfolder3/subfolder(..n)/controller';
OR
// with action method name
$route['default_controller'] = 'subfoler1/subfoler2/subfolder3/subfolder(..n)/controller/action';
If we will pass the action method name it will detect that method as action and if we will not pass action name then it will assume index as a action.
I created a module and there exists a default controller inside that. Now I can access the index action (default action) in the default controller like /mymodule/. For all other action i need to specify the controller id in the url like /mymodule/default/register/ . I would like to know is it possible to eliminate the controller id from url for the default controller in a module.
I need to set url rule like this:
before beautify : www.example.com/index.php?r=mymodule/default/action/
after beautify : www.example.com/mymodule/action/
Note: I want this to happen only for the default controller.
Thanks
This is a little tricky because the action part might be considered as a controller or you might be pointing to an existing controller. But you can get away with this by using a Custom URL Rule Class. Here's an example (I tested it and it seems to work well):
class CustomURLRule extends CBaseUrlRule
{
const MODULE = 'mymodule';
const DEFAULT_CONTROLLER = 'default';
public function parseUrl($manager, $request, $pathInfo, $rawPathInfo)
{
if (preg_match('%^(\w+)(/(\w+))?$%', $pathInfo, $matches)) {
// Make sure the url has 2 or more segments (e.g. mymodule/action)
// and the path is under our target module.
if (count($matches) != 4 || !isset($matches[1]) || !isset($matches[3]) || $matches[1] != self::MODULE)
return false;
// check first if the route already exists
if (($controller = Yii::app()->createController($pathInfo))) {
// Route exists, don't handle it since it is probably pointing to another controller
// besides the default.
return false;
} else {
// Route does not exist, return our new path using the default controller.
$path = $matches[1] . '/' . self::DEFAULT_CONTROLLER . '/' . $matches[3];
return $path;
}
}
return false;
}
public function createUrl($manager, $route, $params, $ampersand)
{
// #todo: implement
return false;
}
}
I'm working on a project built in codeigniter that makes heavy use of routes and the remap function to rewrite urls. The current implementation is confusing and messy.
Essentially this is what the designer was trying to accomplish:
www.example.com/controller/method/arg1/
TO
www.example.com/arg1/controller/method/
Can anyone suggest a clean way of accomplishing this?
This actually only needs to happen for one specific controller. It's fine if all other controllers need to simply follow the normal /controller/model/arg1... pattern
Just to give you an idea of how the current code looks here is the 'routes' file: (not really looking into any insight into this code, just want to give you an idea of how cluttered this current setup is that I'm dealing with. I want to just throw this away and replace it with something better)
// we need to specify admin controller and functions so they are not treated as a contest
$route['admin/users'] = 'admin/users';
$route['admin/users/(:any)'] = 'admin/users/$1';
$route['admin'] = 'admin/index/';
$route['admin/(:any)'] = 'admin/$1';
// same goes for sessions and any other controllers
$route['session'] = 'session/index/';
$route['session/(:any)'] = 'session/$1';
// forward http://localhost/ball/contests to controller contests method index
$route['(:any)/contests'] = 'contests/index/$1';
// forward http://localhost/ball/contests/vote (example) to controller contests method $2 (variable)
$route['(:any)/contests/(:any)'] = 'contests/index/$1/$2';
// forward http://localhost/ball/contests/users/login (example) to controller users method $2 (variable)
$route['(:any)/users/(:any)'] = 'users/index/$1/$2';
// if in doubt forward to contests to see if its a contest
// this controller will 404 any invalid requests
$route['(:any)'] = 'contests/index/$1';
$route['testing/'] = 'testing/';
And the remap function that goes with it:
public function _remap($method, $params = array()){
// example $params = array('ball', 'vote')
// params[0] = 'ball', params[1] = 'vote'
/*
* Write a detailed explanation for why this method is used and that it's attempting to accomplish.
* Currently there is no documentation detailing what you're trying to accomplish with the url here.
* Explain how this moves the contest name url segment infront of the controller url segment. Also
* explain how this works with the routing class.
* */
$count = count($params);
if($count == 0){ // no contest specified
redirect('http://messageamp.com');
return;
}
$contest_name = $params[0];
unset($params[0]); //remove the contest name from params array because we are feeding this to codeigniter
if($count < 2) // no method specified
$method = 'index';
else{
$method = $params[1];
unset($params[1]);
}
//We need to scrap this, lazy-loading is a best-practice we should be following
$this->init(); //load models
//make sure contest is valid or 404 it
if(!$this->central->_check_contest($contest_name)){
show_404();
return;
}
$this->data['controller'] = 'contests';
$this->data['method'] = $method;
$this->data['params'] = $params;
// call the function if exists
if(method_exists($this, $method)){
return call_user_func_array(array($this, $method), $params);
}
show_404(); // this will only be reached if method doesn't exist
}
To get something like this:
www.example.com/controller/method/arg1/ TO www.example.com/arg1/controller/method/
You could do this in your routes.php config:
$route['(:any)/(:any)/(:any)'] = "$2/$3/$1";
However, if you want to have all of your other classes stick to the default routing, you would need to create routes for each of them to overwrite this default route:
$route['controller_name/(:any)'] = "controller_name/$1";
This situation arises from someone wanting to create their own "pages" in their web site without having to get into creating the corresponding actions.
So say they have a URL like mysite.com/index/books... they want to be able to create mysite.com/index/booksmore or mysite.com/index/pancakes but not have to create any actions in the index controller. They (a non-technical person who can do simple html) basically want to create a simple, static page without having to use an action.
Like there would be some generic action in the index controller that handles requests for a non-existent action. How do you do this or is it even possible?
edit: One problem with using __call is the lack of a view file. The lack of an action becomes moot but now you have to deal with the missing view file. The framework will throw an exception if it cannot find one (though if there were a way to get it to redirect to a 404 on a missing view file __call would be doable.)
Using the magic __call method works fine, all you have to do is check if the view file exists and throw the right exception (or do enything else) if not.
public function __call($methodName, $params)
{
// An action method is called
if ('Action' == substr($methodName, -6)) {
$action = substr($methodName, 0, -6);
// We want to render scripts in the index directory, right?
$script = 'index/' . $action . '.' . $this->viewSuffix;
// Script file does not exist, throw exception that will render /error/error.phtml in 404 context
if (false === $this->view->getScriptPath($script)) {
require_once 'Zend/Controller/Action/Exception.php';
throw new Zend_Controller_Action_Exception(
sprintf('Page "%s" does not exist.', $action), 404);
}
$this->renderScript($script);
}
// no action is called? Let the parent __call handle things.
else {
parent::__call($methodName, $params);
}
}
You have to play with the router
http://framework.zend.com/manual/en/zend.controller.router.html
I think you can specify a wildcard to catch every action on a specific module (the default one to reduce the url) and define an action that will take care of render the view according to the url (or even action called)
new Zend_Controller_Router_Route('index/*',
array('controller' => 'index', 'action' => 'custom', 'module'=>'index')
in you customAction function just retrieve the params and display the right block.
I haven't tried so you might have to hack the code a little bit
If you want to use gabriel1836's _call() method you should be able to disable the layout and view and then render whatever you want.
$this->_helper->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
I needed to have existing module/controller/actions working as normal in a Zend Framework app, but then have a catchall route that sent anything unknown to a PageController that could pick user specified urls out of a database table and display the page. I didn't want to have a controller name in front of the user specified urls. I wanted /my/custom/url not /page/my/custom/url to go via the PageController. So none of the above solutions worked for me.
I ended up extending Zend_Controller_Router_Route_Module: using almost all the default behaviour, and just tweaking the controller name a little so if the controller file exists, we route to it as normal. If it does not exist then the url must be a weird custom one, so it gets sent to the PageController with the whole url intact as a parameter.
class UDC_Controller_Router_Route_Catchall extends Zend_Controller_Router_Route_Module
{
private $_catchallController = 'page';
private $_catchallAction = 'index';
private $_paramName = 'name';
//-------------------------------------------------------------------------
/*! \brief takes most of the default behaviour from Zend_Controller_Router_Route_Module
with the following changes:
- if the path includes a valid module, then use it
- if the path includes a valid controller (file_exists) then use that
- otherwise use the catchall
*/
public function match($path, $partial = false)
{
$this->_setRequestKeys();
$values = array();
$params = array();
if (!$partial) {
$path = trim($path, self::URI_DELIMITER);
} else {
$matchedPath = $path;
}
if ($path != '') {
$path = explode(self::URI_DELIMITER, $path);
if ($this->_dispatcher && $this->_dispatcher->isValidModule($path[0])) {
$values[$this->_moduleKey] = array_shift($path);
$this->_moduleValid = true;
}
if (count($path) && !empty($path[0])) {
$module = $this->_moduleValid ? $values[$this->_moduleKey] : $this->_defaults[$this->_moduleKey];
$file = $this->_dispatcher->getControllerDirectory( $module ) . '/' . $this->_dispatcher->formatControllerName( $path[0] ) . '.php';
if (file_exists( $file ))
{
$values[$this->_controllerKey] = array_shift($path);
}
else
{
$values[$this->_controllerKey] = $this->_catchallController;
$values[$this->_actionKey] = $this->_catchallAction;
$params[$this->_paramName] = join( self::URI_DELIMITER, $path );
$path = array();
}
}
if (count($path) && !empty($path[0])) {
$values[$this->_actionKey] = array_shift($path);
}
if ($numSegs = count($path)) {
for ($i = 0; $i < $numSegs; $i = $i + 2) {
$key = urldecode($path[$i]);
$val = isset($path[$i + 1]) ? urldecode($path[$i + 1]) : null;
$params[$key] = (isset($params[$key]) ? (array_merge((array) $params[$key], array($val))): $val);
}
}
}
if ($partial) {
$this->setMatchedPath($matchedPath);
}
$this->_values = $values + $params;
return $this->_values + $this->_defaults;
}
}
So my MemberController will work fine as /member/login, /member/preferences etc, and other controllers can be added at will. The ErrorController is still needed: it catches invalid actions on existing controllers.
I implemented a catch-all by overriding the dispatch method and handling the exception that is thrown when the action is not found:
public function dispatch($action)
{
try {
parent::dispatch($action);
}
catch (Zend_Controller_Action_Exception $e) {
$uristub = $this->getRequest()->getActionName();
$this->getRequest()->setActionName('index');
$this->getRequest()->setParam('uristub', $uristub);
parent::dispatch('indexAction');
}
}
You could use the magic __call() function. For example:
public function __call($name, $arguments)
{
// Render Simple HTML View
}
stunti's suggestion was the way I went with this. My particular solution is as follows (this uses indexAction() of whichever controller you specify. In my case every action was using indexAction and pulling content from a database based on the url):
Get an instance of the router (everything is in your bootstrap file, btw):
$router = $frontController->getRouter();
Create the custom route:
$router->addRoute('controllername', new Zend_Controller_Router_Route('controllername/*', array('controller'=>'controllername')));
Pass the new route to the front controller:
$frontController->setRouter($router);
I did not go with gabriel's __call method (which does work for missing methods as long as you don't need a view file) because that still throws an error about the missing corresponding view file.
For future reference, building on gabriel1836 & ejunker's thoughts, I dug up an option that gets more to the point (and upholds the MVC paradigm). Besides, it makes more sense to read "use specialized view" than "don't use any view".
// 1. Catch & process overloaded actions.
public function __call($name, $arguments)
{
// 2. Provide an appropriate renderer.
$this->_helper->viewRenderer->setRender('overload');
// 3. Bonus: give your view script a clue about what "action" was requested.
$this->view->action = $this->getFrontController()->getRequest()->getActionName();
}
#Steve as above - your solution sounds ideal for me but I am unsure how you implmeented it in the bootstrap?