Switch-based URL routing in PHP - php

What I'm currently doing is this:
I have a $path variable, which is everything after index.php/ (which I hide with .htaccess) up to a question mark to ignore the querystring.
Then I use a switch with preg_match cases on that variable to determine what script it should call. For example:
switch (true)
{
case preg_match('{products/view/(?P<id>\d+)/?}', $path, $params):
require 'view_product.php';
break;
...
default:
require '404.php';
break;
}
This way I can access the product id just using $params['id'] and, if needed, use the querystring for filtering, pagination, etc.
Is there anything wrong with this approach?

You shouldn’t use switch like this.
Better use an array and foreach like:
$rules = array(
'{products/view/(?P<id>\d+)/?}' => 'view_product.php'
);
$found = false;
foreach ($rules as $pattern => $target) {
if (preg_match($pattenr, $path, $params)) {
require $target;
$found = true;
break;
}
}
if (!$found) {
require '404.php';
}

The wrong part would be the switch case . As a better practice i would suggest you store all regex into an array and test it with that . It would be easyer to save the routes to a config file , or an ini file or database or xml or whatever will make you're life easyer in to long run ( if you need to edit/add/delete new routes ) .
In the second part you could use parse_url php function instead of regex witch will speed you're script a bit .

Related

PHP automated GET statement to retrieve content

I have a menu which creates a GET statement in the url
<li>Contact. This get is used to get the corresponding content.
As in, the url will look like ?Page=Contact and than it will load the content from Contact.
Now in another file i have a switch that checks the GET statement in the url.
$GetStatement = $ConfigPage->getFormVariable('Page');
switch ($GetStatement){
case "Home":
$Content = new ContentHome();
$ConfigPage->SetProperty('content', $Content);
break;
case "Contact":
$Content = new ContentContact();
$ConfigPage->SetProperty('content', $Content);
break;
}
Of course there are more cases in this switch, but it's useless to show. Now this switch works flawless. But as my content grows i have to keep adding more cases. And now i am at the point i want this to be automated. Of course i have tried to. but now i have literally no idea how to do this, or what to do.
Edit:
All the different content are in different files. With all unique class name. as you can see above. ContentContact is inside file Contact.php with a class named ContentContact
While that's not a very efficient setup (I don't know that I would create one class per page) what you could do is create a function that would do the work of looking for your class for you
function loadClass($name) {
$class_name = 'Content' . $name;
if(!class_exists($class_name)) return false;
$class = new $class_name();
return $class;
}
$class = loadClass($ConfigPage->getFormVariable('Page'));
if($class) $ConfigPage->SetProperty('content', $class);
Something like this could do the trick:
function getContentInstance($stmt) {
$name = 'Content'.$stmt;
$path = 'Content/'.str_replace(".", "", $name).'.class.php'; // needs to do even more ...
if(class_exists($name) {
return new $name();
} else {
if (file_exists($path)) {
include $path;
return new $name();
}
user_error('Class '.$name.' not found');
}
}

Is it wrong to use the return statement this way?

I've got this autoloader method which is used to include class files as needed.
public static function autoloader($className) {
$fileExists = false;
foreach(array(LIBS_PATH, CONTROLLERS_PATH, MODELS_PATH) as $path) {
// Check if the class file exists and is readable
if(is_readable($classPath = $path . '/' . $className . '.php')) {
// Include the file
require($classPath);
$fileExists = true;
break;
}
}
if($fileExists === false) {
exit('The application cannot continue as it is not able to locate a required class.');
}
}
That works fine but I was thinking is this way better:
public static function autoloader($className) {
foreach(array(LIBS_PATH, CONTROLLERS_PATH, MODELS_PATH) as $path) {
// Check if the class file exists and is readable
if(is_readable($classPath = $path . '/' . $className . '.php')) {
// Include the file
require($classPath);
return;
}
}
exit('The application cannot continue as it is not able to locate a required class.');
}
As you can see I'm using the return statement in the middle of the loop to terminate the rest of the function because the class file has been included and the method has done its job.
Which way is the best way to break out of the loop if a match has been found? I'm just unsure about using the return statement because I always associated that with returning some value from a method.
Return can be used to simply break out of a method/function, and this use is perfectly legal. The real question to ask is what impact do you feel it has on readability?
There are different schools of thought on early returns.
One school maintains a single entry/exit point, stating that it makes the code easier to read since one can be assured that logic at the bottom of the code will be reached.
Another school states that the first school is outdated, and this is not as pressing a concern, especially if one maintains shorter method/function lengths.
A medium exists between the two where a trade-off between an early return and convoluted logic is present.
It comes down to your judgment.
No, it is not wrong to use return this way.
At least look at manual:
http://php.net/manual/en/function.autoload.php
void __autoload ( string $class )
void means that it should not return anything.
But it is not an error.
And also better use require_once when including class definitions.

PHP Use Include Inside Function

Im trying to make a function that I can call as follows,
view( 'archive', 'post.php' );
and what the function really does is this.
include( 'view/archive/post.php' );
The reason for this is if in the future I expand the directory to be view/archive/another_level/post.php I dont want to have to go back everywhere in my code and change all the include paths.
Currently this is what i have for my function, except it appears that the include is being call inside the function, and not being called when the function is called...
function view( $view, $file )
{
switch ( $view )
{
case 'archive' : $view = 'archive/temp'; break;
case 'single' : $view = 'single'; break;
}
include( TEMPLATEPATH . "/view/{$view}/{$file}" );
}
How can I get this function to properly include the file?
EDIT:
There were no errors being displayed. Thanks to #Ramesh for the error checking code, ini_set('display_errors','On') I was able to see that there were other 'un-displayed' errors on the included file, which appeared to have caused the file not to show up...
That use case is explicitly documented:
If the include occurs inside a function within the calling file, then
all of the code contained in the called file will behave as though it
had been defined inside that function. So, it will follow the variable
scope of that function. An exception to this rule are magic constants
which are evaluated by the parser before the include occurs.
IMHO, it's way simpler to keep base paths in constants (you already seem to be doing it to some extent) or even make a full-site search and replace (which is a 30 second task in any decent editor) than rewriting all your included files to use global variables.
Here's one way you could solve the problem:
change the way you call your function so it looks like this:
include( view('archive','post') );
and your function would look like this:
<?php
function view( $view, $file )
{
switch ( $view )
{
case 'archive': $view = 'archive/temp'; break;
case 'single' : $view = 'single'; break;
}
return TEMPLATEPATH . "/view/{$view}/{$file}";
}
?>
While you don't actually state what the exact problem you are having is, I suspect that it is that variables are not available to your included file. A slightly horrible way to partially solve this problem is to add this line before your include statement:
extract($GLOBALS);
This will import all variables from the global scope into your function. However this will not make the function do exactly what you want. Consider this code:
function some_func () {
$x = 2;
view('archive', 'post.php');
}
$x = 1;
some_func();
In the included file, the value of $x will be 1, not 2 as you would want/expect. This is because $GLOBALS only ever contains data from the global scope, it does not contain the variables from the scope of some_func(). There is no mechanism for accessing the variables in the "parent" scope in PHP.
The long of the short of this is that the approach you want to use (wrapping it in a function) will not work.
I think you should read about the variable scope.
http://php.net/manual/en/language.variables.scope.php
The scope of a variable is the context within which it is defined.
So, if you include a file inside a function, you'll have its contents available only in the context of that function.
function view( $view, $file )
{
switch ( $view )
{
case 'archive' : $view = 'archive/temp'; break;
case 'single' : $view = 'single'; break;
}
include( TEMPLATEPATH . "/view/".$view."/".$file );
}
this works for me. Also you could use the include inside the case or even better built the whole url to include inside the case each time.

How to handle 404's with Regex-based routing?

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.

Is there a Symfony helper for getting the current action URL and changing one or more of the query parameters?

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();

Categories