How would I use PHP exceptions to define a redirect?
I've always been taught that using exceptions in programming allowed for error handling to be abstracted from the objects that throw the errors. Looking at the PHP manual, it seems that PHP has an Exception class and an ErrorException class, indicating that not all exceptions have to be errors. So, I'd like to use them to help with page redirects.
I want to have a hard redirect that will send only a header and no page content. What would be the best way to trigger this? Let's say I have a Controller
class with a redirect()
method.
Should that method look like this:
class Controller {
public function redirect($path) {
throw new Exception($path, 301);
}
}
...
try {
$controller->redirect('http://domain.tld/redirected');
} catch (Exception $e) {
if ($e->getCode() == 301) {
header('Location: ' . $e->getMessage());
}
}
Or like this:
class Controller {
public function redirect($path) {
header('Location: ' . $path);
throw new Exception('The page is being redirected', 301);
}
}
...
try {
$controller->redirect('http://domain.tld/redirected');
} catch (Exception $e) {
if ($e->getCode() == 301) {
// Output nothing
}
}
Or should I create a new type of exception like this:
class RedirectException extends Exception {
protected $url;
public function __construct($url) {
parent::__construct('The redirects are coming!', 301);
$this->url = (string)$url;
}
public function getURL() {
return $this->url;
}
}
...
class Controller {
public function redirect($path) {
throw new RedirectException($path);
}
}
...
try {
$controller->redirect('http://domain.tld/redirected');
} catch (RedirectException $e) {
header('Location: ' . $e->getURL());
}
While I feel like all of these would work, none of them feel right to me. The last seems the closest since it makes it clear that the URL is a required member. However, an exception like that would serve only one purpose. Would it make more sense to build a RequestException that handles all 3X开发者_StackOverflow社区X, 4XX, and 5XX status codes? Plus, what about the message? Does that just become extraneous information at this point?
I've been playing around with this myself too. I'll share my thoughts on the matter.
Rationale
Q: Why would someone use exceptions to redirect at all, when you can do it just as easy using a header
and die
statement?
A: RFC2616 has this to say about redirecting with status code 301:
Unless the request method was HEAD, the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s).
Because of this, you actually need some code to implement a redirect correctly. It is better to implement this once and make it easily accessible for reuse.
Q: But then you could just as easy implement a redirect
method, you wouldn't need an Exception.
A: When you redirect, how can you know it is "safe" to kill the PHP script with a die
? Maybe there is some code down the stack that is waiting for you to return, so it can run some cleanup operations. By throwing an Exception, code down the stack can catch this exception and cleanup.
Comparison
Your example #1 and #3 are actually the same, with the difference that in #1 you are abusing the generic Exception
class. The name of the exception should say something about what it does (RedirectException
), not an attribute (getCode() == 301
), especially because nowhere is defined that the code in an exception should match the HTTP Status code. Additionally, code wanting to catch redirects in situation #1 cannot simply do catch (RedirectException $re)
but will need to check the result of getCode()
. This is unnecessary overhead.
The most important difference between #2 and #3 is how much control you give to the classes receiving the exception. In #2 you pretty much say "A redirect is coming, this is happening", the catch block has no reliable way to prevent the redirect from happening. While in #3 you say "I want to redirect, unless you have some better idea", a catch block will stop ("catch") the redirect, and it won't happen until the exception is thrown further down the stack.
Which to choose
It depends how much control you want to give code down the stack. I personally think that code down the stack should be able to cancel a redirect, which would make #3 a better choice. A typical use case for this is a redirect to a login page: Imagine a method that will do some operation for the current user, or redirect to the login page if nobody is logged in. This method may be called from a page that does not require that a user is logged in, but will give extra functionality if there is. Just catching an exception is a lot cleaner than writing code around the method, just to check if a user is actually logged in.
Some programmers might pick #2, because they think that if some code initiates a redirect, it expects that this redirect will actually happen. Allowing to intercept the redirect and do something else will make the framework less predictable. However, I tend to think that this is what exceptions are about; unless some code has a way to handle the exception, the operation associated with the exception happens. This operation usually is the display of an error message, but it can be something else, like a redirect.
Example implementation of #3
class RedirectException extends Exception {
const PERMANENT = 301;
const FOUND = 302;
const SEE_OTHER = 303;
const PROXY = 305;
const TEMPORARY = 307;
private static $messages = array(
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
);
protected $url;
public function __construct($url, $code = 301, $message = NULL) {
parent::__construct($message
? (string)$message
: static::$messages[$code], (int)$code
);
if (strpos($url, '/') === 0) {
$this->url = static::getBaseURL() . $this->url;
}
$this->url = (string)$url;
}
public function getURL() {
return $this->url;
}
public function run() {
header('Location: ' . $this->url, true, $this->getCode());
}
}
Conclusion
Example #1 and #3 are almost the same, but #3 is better design. #2 and #3 are both good solutions, depending on what your requirements are. Example #2 will allow code down the stack to react to a redirect, but will not be able to prevent this from happening. Example #3 will also allow code down the stack to react, but it will also enable the same code to prevent the redirect from happening.
The first method is the best of the 3 methods. The post made by Marc B has a very valid point but you might already have solution for this not mentioned in the code.
- Always die() after redirecting.
- Using the message of an exception to hold the URL is not very OOP. If you want to use exceptions you should make a custom exception that can contain a URL.
Note: Because you should die after a redirect you might see why example 2 and 3 are weird.
Explanation: If you request a page the first thing the server sends to you is the header. The header contains information about the webpage you are about to receive. The first statement in your php code that generates output or the first HTML code in your website triggers the header to be send. If you put the header location statement in your code the header is altered to tell the browser to immediately redirect to the URL mentioned. So pretty much all execution after the header statement is useless and skipped.
精彩评论