My main concern is what i'm missing here. The route worked perfectly for what i've tested, after i started to add content on the website it stopped working
$route['projects/view/([A-Za-z0-9])/task/([A-Za-z0-9])'] = 'Projects/view_project_task/$1/$2';
Function in model is defined like:
public function view_project_task($project_id, $project_task_id)
If i access it like http://mydomain/projects/view_project_task/1/8 is working perfectly fine, if i try to access like http://mydomain/projects/view/1/task/8 i get a 404 error on some linkes and it works on others.
if you try using callbacks in your route?
$route['projects/(.+)'] = function ($params)
{
$return = explode('/', $params);
//here you need to count how many params has to know if your url
//is like 1/8 -> [0]=>view_project_task [1]=>1 [2]=>8
//or like 1/task/8 [0]=>view [1]=>1 [2]=>task [3]=>8
return 'projects/view_project_task/' . strlower($return[1]) . '/' . ( count($return > 3)) ? $return[3] : $return[2];
};
Related
OK, I think I am probably (hopefully) going to be told I am going about this in the wrong way.
Currently, if I go to root of CI web I call a function which reads a predefined location with map_directory(). I then iterate it out in a view as a simple directory listing.
I then want to click on one of these directories to see what's inside. When I do that I call a different controller function called browse.
So if I click on one link I go to
www.mysite.com/dir1
(which is Routed to www.mysite.com/controller/browse/$1 - where $1 in this instance = dir1).
Now I am presented with a dir listing of dir1. I have configured the links of the displayed listings to now go to:
www.mysite.com/dir1/subdir1 etc.
What I want to do and this might be the bit where I am cheating/going wrong, is capture everything after
www.mysite.com/
and pass it to
www.mysite.com/controller/browser/$1
So example:
www.mysite.com/dir1/subdir1/ => www.mysite.com/controller/browse/"dir1/subdir1"
I know I can't have '"' in there, but that's the bit I am trying to pass to the map_directory() function...so it goes /.$var (where $var would = $1 = "dir1/subdir1".
So far I have tried in CI Routes.php:
$route['(.+)$'] = 'controller/browse/$1';
$route['([a-zA-Z0-9\/]*)'] = 'controller/browse/$1';
$route['(:any)'] = 'controller/browse/$1';
...but they all only ever seem to capture "dir1" never anything beyond that.
I hope that makes sense to someone....
You can just use the special _remap() method in your controller to override the default routing behavior:
class Controller extends CI_Controller
{
protected function browse($path)
{
// ... do something with $path here
}
public function _remap($method, $params = array())
{
if ($method === 'browse')
{
$params = implode('/', $params);
return $this->browse($params);
}
elseif (method_exists($this, $method))
{
return call_user_func_array(array($this, $method), $params);
}
show_404();
}
}
My function in users class:
public function form($a = false, $b = false, $c= false)
{
// Something to do
}
My request uri:
..admin/users/form/1/2/3
I'm getting 404 error:
404 Page Not Found
The page you requested was not found.
But If i try alphabetical characters like admin/users/form/1/something/1 instead of numeric 2 or 1 places, it works.
So;
..admin/users/form/1/2 > works
..admin/users/form/1/2/3 > not work
..admin/users/form/a/2/3 > works
..admin/users/form/1/a/3 > works
..admin/users/form/1/2/a > not work
And i tried with custom routes and remapping but again i couldnt figure out the issue.
Have you tried
$route['admin/users/form(/:any)*'] = 'admin/users/form';
Then use uri segments in your controller:
public function form()
{
$a = $this->uri->segment(4);
$b = $this->uri->segment(5);
$c = $this->uri->segment(6);
}
I'm not sure why your initial setup isn't working because I always use routes this way. Works fine for me.
Please consider the following very rudimentary "controllers" (functions in this case, for simplicity):
function Index() {
var_dump(__FUNCTION__); // show the "Index" page
}
function Send($n) {
var_dump(__FUNCTION__, func_get_args()); // placeholder controller
}
function Receive($n) {
var_dump(__FUNCTION__, func_get_args()); // placeholder controller
}
function Not_Found() {
var_dump(__FUNCTION__); // show a "404 - Not Found" page
}
And the following regex-based Route() function:
function Route($route, $function = null)
{
$result = rtrim(preg_replace('~/+~', '/', substr($_SERVER['PHP_SELF'], strlen($_SERVER['SCRIPT_NAME']))), '/');
if (preg_match('~' . rtrim(str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $route), '/') . '$~i', $result, $matches) > 0)
{
exit(call_user_func_array($function, array_slice($matches, 1)));
}
return false;
}
Now I want to map the following URLs (trailing slashes are ignored) to the corresponding "controllers":
/index.php -> Index()
/index.php/send/:NUM -> Send()
/index.php/receive/:NUM -> Receive()
/index.php/NON_EXISTENT -> Not_Found()
This is the part where things start to get tricky, I've two problems I'm not able to solve... I figure I'm not the first person to have this problem, so someone out there should have the solution.
Catching 404's (Solved!)
I can't find a way to distinguish between requests to the root (index.php) and requests that shouldn't exist like (index.php/notHere). I end up serving the default index.php route for URLs that should otherwise be served a 404 - Not Found error page. How can I solve this?
EDIT - The solution just flashed in my mind:
Route('/send/(:num)', 'Send');
Route('/receive/(:num)', 'Receive');
Route('/:any', 'Not_Found'); // use :any here, see the problem bellow
Route('/', 'Index');
Ordering of the Routes
If I set up the routes in a "logical" order, like this:
Route('/', 'Index');
Route('/send/(:num)', 'Send');
Route('/receive/(:num)', 'Receive');
Route(':any', 'Not_Found');
All URL requests are catched by the Index() controller, since the empty regex (remember: trailing slashes are ignored) matches everything. However, if I define the routes in a "hacky" order, like this:
Route('/send/(:num)', 'Send');
Route('/receive/(:num)', 'Receive');
Route('/:any', 'Not_Found');
Route('/', 'Index');
Everything seems to work like it should. Is there an elegant way of solving this problem?
The routes may not always be hard-coded (pulled from a DB or something), and I need to make sure that it won't be ignoring any routes due to the order they were defined. Any help is appreciated!
Okay, I know there's more than one way to skin a cat, but why in the world would you do it this way? Seems like some RoR approach to something that could be easily handled with mod_rewrite
That being said, I rewrote your Route function and was able to accomplish your goal. Keep in mind I added another conditional to catch the Index directly as you were stripping out all the /'s and that's why it was matching the Index when you wanted it to match the 404. I also consolidated the 4 Route() calls to use a foreach().
function Route()
{
$result = rtrim(preg_replace('~/+~', '/', substr($_SERVER['PHP_SELF'], strlen($_SERVER['SCRIPT_NAME']))), '/');
$matches = array();
$routes = array(
'Send' => '/send/(:num)',
'Receive' => '/receive/(:num)',
'Index' => '/',
'Not_Found' => null
);
foreach ($routes as $function => $route)
{
if (($route == '/' && $result == '')
|| (preg_match('~' . rtrim(str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $route)) . '$~i', $result, $matches) > 0))
{
exit(call_user_func_array($function, array_slice($matches, 1)));
}
}
return false;
}
Route();
Cheers!
This is a common problem with MVC webapps, that is often solved before it becomes a problem at all.
The easiest and most general way is to use exceptions. Throw a PageNotFound exception if you don't have a content for given parameters. At the top level off your application, catch all exceptions like in this simplified example:
index.php:
try {
$controller->method($arg);
} catch (PageNotFound $e) {
show404Page($e->getMessage());
} catch (Exception $e) {
logFatalError($e->getMessage());
show500Page();
}
controller.php:
function method($arg) {
$obj = findByID($arg);
if (false === $obj) {
throw new PageNotFound($arg);
} else {
...
}
}
The ordering problem can be solved by sorting the regexes so that the most specific regex is matched first, and the least specific is matched last. To do this, count the path separtors (ie. slashes) in the regex, excluding the path separator at the beginning. You'll get this:
Regex Separators
--------------------------
/send/(:num) 1
/send/8/(:num) 2
/ 0
Sort them by descending order, and process. The process order is:
/send/8/(:num)
/send/(:num)
/
OK first of all something like:
foo.com/index.php/more/info/to/follow
is perfectly valid and as per standard should load up index.php with $_SERVER[PATH_INFO] set to /more/info/to/follow. This is CGI/1.1 standard. If you want the server to NOT perform PATH_INFO expansions then turn it off in your server settings. Under apache it is done using:
AcceptPathInfo Off
If you set it to Off under Apache2 ... It will send out a 404.
I am not sure what the IIS flag is but I think you can find it.
What I'd like to do is take the route for the current action along with any and all of the route and query string parameters, and change a single query string parameter to something else. If the parameter is set in the current request, I'd like it replaced. If not, I'd like it added. Is there a helper for something like this, or do I need to write my own?
Thanks!
[edit:] Man, I was unclear on what I actually want to do. I want to generate the URL for "this page", but change one of the variables. Imagine the page I'm on is a search results page that says "no results, but try one of these", followed by a bunch of links. The links would contain all the search parameters, except the one I would change per-link.
Edit:
Ok I got a better idea now what you want. I don't know whether it is the best way but you could try this (in the view):
url_for('foo',
array_merge($sf_request->getParameterHolder()->getAll(),
array('bar' => 'barz'))
)
If you use this very often I suggest to create your own helper that works like a wrapper for url_for.
Or if you only want a subset of the request parameters, do this:
url_for('foo',
array_merge($sf_request->extractParameters(array('parameter1', 'parameter3')),
array('bar' => 'barz'))
)
(I formated the code this way for better readability)
Original Answer:
I don't know where you want to change a parameter (in the controller?), but if you have access to the current sfRequest object, this should do it:
$request->setParameter('key', 'value')
You can obtain the request object by either defining your action this way:
public function executeIndex($request) {
// ...
}
or this
public function executeIndex() {
$request = $this->getRequest();
}
For symfony 1.4 I used:
$current_uri = sfContext::getInstance()->getRouting()->getCurrentInternalUri();
$uri_params = $sf_request->getParameterHolder()->getAll();
$url = url_for($current_uri.'?'.http_build_query(array_merge($uri_params, array('page' => $page))));
echo link_to($page, $url);
Felix's suggestion is good, however, it'd require you to hard core the "current route"..
You can get the name of the current route by using:
sfRouting::getInstance()->getCurrentRouteName()
and you can plug that directly in url_for, like so:
url_for(sfRouting::getInstance()->getCurrentRouteName(),
array_merge($sf_request->extractParameters(array('parameter1', 'parameter3')),
array('bar' => 'barz'))
)
Hope that helps.
With the same concept than Erq, and thanks to his code, I have made the same with some small changes, since my URL needs to convert some characters. Its generic though and should work with most forms, in order to save the parameters the user has chosen to search for.
public function executeSaveFormQuery(sfWebRequest $request)
{
$sURLServer = "http://";
$sURLInternalUri = "";
$page = "";
$sURLInternalUri = sfContext::getInstance()->getRouting()->getCurrentInternalUri();
$suri_params = $request->getParameterHolder()->getAll();
$sParams = http_build_query(array_merge($suri_params));
$dpos = strpos($sURLInternalUri, "?");
$sURLConsulta[$dpos] = '/';
$sURL = substr($sURLInternalUri, $dpos);
$dpos = strpos($sURL, "=");
$sURL[$dpos] = '/';
$sURLFinal = $sURLServer . $sURL . '?' . $sParams;
//$this->redirect($this->module_name . '/new');
self::executeNew($request, $sURLFinal);
//echo "var_dump(sURLFinal): ";
//var_dump($sURLFinal);
//echo "<br></br>";
//return sfView::NONE;
}
In executeNew, as easy as:
public function executeNew(sfWebRequest $request, $sURLQuery)
{
//$sURLQuery= "http://";
if ($sURLQuery!= "")
{
$this->form = new sfGuardQueryForm();
//echo "var_dump(sURLQuery)";
//var_dump($sURLQuery);
//echo "<br></br>";
$this->form->setDefault('surl', $sURLQuery);
}
else
{
$this->form = new sfGuardQueryForm();
}
}
echo $sf_context->getRequest()->getUri();
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?