Registering Database connection in container: exit gracefully if can't connect?

I’ve been reading this article, but I can’t figure out a way to exit gracefully (i.e. with a custom Response body and a 500 status code) from within a container. Say that I create a database connection like so:

$container['pdo'] = function ($container) {
    $cfg = $container->get('settings')['db'];
    return new \PDO($cfg['dsn'], $cfg['user'], $cfg['password']);
};

If new PDO() fails, how do I exit gracefully? Right now it returns NULL, so it trickles all the way down to my Model classes. My only solution for now is to not use a container at all, and have my Service class itself instantiate the PDO.

Are you using PHP 7+? There are a few ways to trigger an error, but probably the easiest will be to set a return type on that function. Just add a : \PDO before the opening { and if the function doesn’t return a valid PDO object you’ll get a 500

You can also enable PDO exceptions, which should throw if the connection fails.

After the password, add this array:

[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]

Thanks for the info. I mentioned PDO b/c that’s what the article was using, but I’m not using PDO, so I don’t know if the following is valid code for it. However I realized that just by overriding the default $container['errorHandler'] it’s now possible to throw from the container itself, so:

$container['errorHandler'] = function ($container) {
   return new CustomHandler($container->logger);
};

$container['pdo'] = function ($container) {
   $cfg = $container->get('settings')['db'];
   $pdo = new \PDO($cfg['dsn'], $cfg['user'], $cfg['password']);

   if (!isset($pdo))
       throw new CustomException(500, 'DB connection unavailable');

   return $pdo; 
};

class CustomHandler{
 protected $logger;

 public function __construct($logger){
   $this->logger = $logger;
 }

 public function __invoke($request, $response, $exception){
   if (is_a($exception, CustomException::class)){
     /*log and create a new custom response with custom exception params*/
     return $custom_response;
   }
   return $response;
 }
}

It’s hard to “guess” if we don’t see the real code here. But I would like to give you some feedback about the current example code (which is not the real code).

  • Today it’s not best practice to check for errors every time. It’s better to let the exception just “happen” and then let it “bubble up” to the next error handler. In Slim 3 the default errors handler are errorHandler and phpErrorHandler for PHP exceptions. For this reason it’s better to configure the database handler in “exception mode”.
  • Let the error handler care about the transformation of the exception to the correct HTTP status code + logging etc…
  • Using is_a is deprecated (since PHP 5.0.0) in favour of the instanceof operator. Calling this function will result in an E_STRICT warning.
  • Using custom strings for container entries (like ‘pdo’ or ‘logger’) is a little bit “outdated”. Better make use of the fully qualified class name. See also ::class.

Example:

use Psr\Container\ContainerInterface as Container;
use Psr\Log\LoggerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Monolog\Logger;

$container[LoggerInterface::class] = function (Container $container) {
    $settings = $container->get('settings');
    $logger = new Logger($settings['logger']['name']);

    $level = $settings['logger']['level'];
    if (!isset($level)) {
        $level = Logger::ERROR;
    }
    $logFile = $settings['logger']['file'];
    $handler = new RotatingFileHandler($logFile, 0, $level, true, 0775);
    $logger->pushHandler($handler);

    return $logger;
};

// Slim Framework application error handler
$container['errorHandler'] = function (Container $container) {
    $logger = $container->get(LoggerInterface::class);

    return function(Request $request, Response $response, Throwable $exception) use ($logger) {
        $logger->error($exception->getMessage());
        
        return $response->withStatus(500)
            ->withHeader('Content-Type', 'text/html')
            ->write('Something went wrong!');
    };
};

// PHP exceptions handler
$container['phpErrorHandler'] = function (Container $container) {
    return $container->get('errorHandler');
};

Thanks for the feedback.

Our errorHandlers are pretty similar, except I’m using an invokable class.

I also want to specify which logger and level to use for each error/exception that gets thrown, so it was easier for me to create a bunch of ‘custom exceptions’ and throw them where I needed them to be (instead of letting slim take care of it), like these:

abstract class CUSTOM_EXCEPTIONS {
	#[USER-FACING]						 status code level   logger      message
	const deprecated 				    = [410, 100, 'WARN', API_LOGGER, "API for current App version was either deprecated or not found"];
	const invalidCreds			        = [401, 101, 'INFO', API_LOGGER, "Invalid e-mail/password"];

	#[API]
	const questionableHash 				= [400, 200, 'WARN', API_LOGGER, "Password does not seem to be a hash generated by the App"];
	const questionableAuthToken 		= [400, 201, 'WARN', API_LOGGER, "Auth Token does not seem to be generated by the API"];

	#[DB]
	const dbConnectionFailed 		    = [503, 301, 'ALERT', DB_LOGGER, "DB connection error"];
	const dbQueryError				    = [500, 302, 'ERROR', DB_LOGGER, "DB query error"];
}

This way my CustomHandler check first against a list of pre-made custom exceptions; if it’s not a custom exception it gets passed to the default logger and return with $response->withStatus(500)->withHeader(...)->write(...).

For instance, I decided to implement my own custom exception if my database fails to connect (which is why I created this Discourse topic) so I can send a 503 back instead of the default 500. Also, if I didn’t check for my database connection to be not null, PHP will throw an error (instead of the db connection) on the first query saying ‘cannot execute method xxx on null’, which doesn’t really explain why it was null in the first place. So in this case letting the error ‘bubble’ wouldn’t help me figure out what’s wrong.

So my idea is to intercept any custom exception first, then let anything that’s not custom become the default 500 status code with default logging and response.

You don’t think this is ok?