5

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 3XX, 4XX, and 5XX status codes? Plus, what about the message? Does that just become extraneous information at this point?

5
  • ...cont..Still not completely reliable, but still has a better chance of causing a redirect, regardless of what state your document is in. Commented Aug 17, 2011 at 14:30
  • Not going to touch the actual question, but if you want to use redirects in this manner, you'd better have some checks to headers_sent(). Your exception handlers can potentially kick in at any stage of a page's creation, and the redirect would only work if the exception occurs BEFORE any output. Otherwise you'll just get the "headers already sent" warnings and no redirect will occur. A somewhat safer usage would be: if (headers_sent()) { ... output a <meta> redirect + javascript document.location block ... } else { header('Location: ...'); } Commented Aug 17, 2011 at 14:30
  • thanks marc for the tip, its a totally new thing to me, but, just out of curiosity, why would you do a meta redirect + JS block? Just to be on safer side? Commented Aug 17, 2011 at 15:44
  • @Kumar: Yes, safer side. If your exception MUST do a redirect, then adding multiple layers of redirect attempts makes it that much more likely that the redirect will occur. Commented Aug 17, 2011 at 15:47
  • The idea that an exception is raised in anything other than an exceptional circumstance is pretty much an anti-pattern to me Commented Jul 28, 2015 at 14:18

2 Answers 2

5

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Your argument about it allowing for clean up code to be run is just as possible with a redirect method.
That is correct. This can be done by putting the cleanup code into the redirect method, but that requires that business logic to be present in the method. Alternatively you could use callback functions, but that requires bookkeeping. #3 will also allow calling code to cancel the redirect (Comparison, last paragraph).
0

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.

1 Comment

The point of not using die() would be to handle the cases in which a header redirect is impossible. I didn't want to bog down the code samples with the extra logic since the post is already long. While I've shown the use of header(), I'm also considering the option of displaying a page that says "Redirecting to X in 10 seconds" and then having the countdown update for JavaScript-enabled browsers.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.