如果缺少数据库,则设计模式可以正常失败

I have an app under development which uses a database for a subset of its functionality. During development we have a higher than normal expectation that the database won't be available. Once launched, we would still like the main body of the application to work even if there is a database outage.

All of my database-facing classes extend a base class which grants them database access, however in the concrete classes there are some methods which perform selects directly (meaning not through the base classes methods).

Short of wrapping every database call in if statements, how can I gracefully degrade functionality when the database is not available?

EDIT

Failing gracefully for this app would be displaying a "service unavailable" message and stopping classes from attempting to access the database (which would throw errors).

IMHO this falls under the wider scope of general error and exception handling.

Some intro ...

The database is essentially a global variable input in your code. If it's unavailable this is the very definition of an unexpected, "exceptional" condition; it should result in an exception. Of course, you explicitly asked how to handle the situation "short of wrapping every database call in if statements." Wrapping each and every DB operation in a try/catch block is basically the same as wrapping each operation in if statements.

Further, the mysql_connect function will raise a PHP error in addition to returning FALSE if it can't connect. You should really be using PDO instead of the mysql dinosaur, but that's another topic entirely. Hopefully you aren't using the error suppression operator @ or not reporting E_WARNING errors to avoid this.

So how to handle it ...

Personally, I would have nothing to do with error handling in PHP. You should define a custom error handler function using set_error_handlerdocs that throws an exception on any PHP error and handle exceptions instead of errors.

When you set up your error handling strategy in this manner it's then simple to wrap any functionality that could cause problems or unexpectedly throw in a try block and handle the unexpected events in your catch blocks.

Since you stated that, "All of [your] database-facing classes extend a base class which grants them database access," I would suggest adding a simple boolean property to the base class to act as a flag for whether or not the database connection has failed. Then, just check the value of the flag before each attempt to query the database. If you're correctly passing around the database instance in your classes (and not breaking references), they'll all have access to the same property when they need it.

If the database connection failed, you would just set the hypothetical $dbConn->connError = TRUE; flag in the try block.

So, to summarize, while the specifics of how to structure the actual code depend on exactly what you're doing, using exceptions to handle error conditions is IMHO the most robust way to go about it.

Is AOP a possibility? That could allow you to write code that intercepts any part of code that connects to the DB, and insert your error handling code.

http://code.google.com/p/php-aop/

I don't think retrofitting my solution in a project is easily done, but I do it like this:

When I want a failure to be recognized as critical in my application, I use Exceptions. In my error handler I have some logic which determines whether the exceptions matches a http-status code like 4xx, if not a generic 500 status code is generated.

Non-critical failures are captured by a less disruptive mechanism using a custom result-class. It's basic structure is similar to an Exception, in that a code and message are passed as constructor-arguments. Additionally, a result-object can be passed as an optional argument.

The Result-class looks like this:

class Result
{
    /**
     * @var int
     */
    protected $resultCode;

    /**
     * @var string
     */
    protected $resultMessage;

    /**
     * @var mixed
     */
    protected $resultObject;

    /**
     * Inject object values on construction.
     *
     * @param int $code
     * @param string $message
     * @param mixed $result
     */
    public function __construct($code = 0, $message = null, $result = null)
    {
        $this->resultCode = intval($code);
        $this->resultMessage = (string) $message;
        $this->resultObject = $result;
    }

    /**
     * Returns true when result is considered a success, determined by the
     * resultCode, otherwise false.
     *
     * @return bool
     */
    public function isSuccess()
    {
        if ($this->resultCode > 0) {
            return true;
        }
        return false;
    }


    /**
     * Returns true when result is considered an error, determined by the
     * resultCode, otherwise false.
     *
     * @return bool
     */
    public function isError()
    {
        if ($this->resultCode < 1) {
            return true;
        }
        return false;
    }

    /**
     * Returns the result message.
     *
     * @return string
     */
    public function getMessage()
    {
        return $this->resultMessage;
    }

    /**
     * Returns the actual result object.
     *
     * @return mixed
     */
    public function getResult()
    {
        return $this->resultObject;
    }
}

Only the result code is required. The result is considered successful if the code is > 0, otherwise it is considered to be an error. The result-object can either be the actual result (if successful), e.g. a row or rowset from a query or (if error) the input or parts of the input, which are relevant, e.g. the row-id you were looking for.

As this result-class does not trigger the errorHandler, the MVC-application is trusted to handle the errors. either by throwing an exception, or gracefully reacting to the failure-result by showing a message or whatever was passed as result-object.

If I pass this result to my view I can react whether $result->isSuccess() is true or false and just get the actual result via $result->getResult() or display an error message via $result->getMessage().