4

I'm working on a fairly large JSON API using Slim3. My controllers/actions are currently littered with the following:

return $response->withJson([
    'status' => 'error',
    'data' => null,
    'message' => 'Username or password was incorrect'
]);

At certain points in the application anything can go wrong and the response needs to be appropriate. But one thing that is common is the error responses are always the same. The status is always error, the data is optional (in the case of form validation errors data will contain those) and message is set to indicate to the user or consumer of the API what went wrong.

I smell code duplication. How can I reduce the code duplication?

From the top of my head all I could think of doing was creating a custom exception, something like App\Exceptions\AppException that takes option data and the message will be obtained form $e->getMessage().

<?php

namespace App\Exceptions;

class AppException extends Exception
{
    private $data;

    public function __construct($message, $data = null, $code = 0, $previous = null)
    {
        $this->data = $data;
        parent::__construct($message, $code, $previous);
    }

    public function getData()
    {
        return $this->data;
    }
}

Following that create middleware that calls $next wrapped in a try/catch:

$app->add(function($request, $response, $next) {

  try {
    return $next($request, $response);
  }
  catch(\App\Exceptions\AppException $e)
  {
    $container->Logger->addCritical('Application Error: ' . $e->getMessage());
    return $response->withJson([
      'status' => 'error',
      'data' => $e->getData(),
      'message' => $e->getMessage()
    ]);
  }
  catch(\Exception $e)
  {
    $container->Logger->addCritical('Unhandled Exception: ' . $e->getMessage());
    $container->SMSService->send(getenv('ADMIN_MOBILE'), "Shit has hit the fan! Run to your computer and check the error logs. Beep. Boop.");
    return $response->withJson([
      'status' => 'error',
      'data' => null,
      'message' => 'It is not possible to perform this action right now'
    ]);
  }
});

Now all I have to do at points in the code is to throw new \App\Exceptions\AppException("Username or password incorrect", null).

My only issue with this is it feels like I'm using exceptions for the wrong reasons and it may make debugging a little more difficult.

Any suggestions on reducing the duplicates and cleaning up error responses?

BugHunterUK
  • 8,346
  • 16
  • 65
  • 121
  • You are using exceptions just like you should in Slim. I've built a lot of APIs with Slim, and used the exactly same method - a specific AppException that I throw when I want to stop the request processing and I let it get caught in the exceptionHandler of Slim. This is the cleanest and must uncoupled way I've found. Just use @Mika Tuupola method. – Ron Dadon Dec 13 '16 at 09:17

1 Answers1

5

You can achieve similar similar results by creating an error handler which outputs JSON.

namespace Slim\Handlers;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

final class ApiError extends \Slim\Handlers\Error
{
    public function __invoke(Request $request, Response $response, \Exception $exception)
    {
        $status = $exception->getCode() ?: 500;
        $data = [
            "status" => "error",
            "message" => $exception->getMessage(),
        ];
        $body = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        return $response
                   ->withStatus($status)
                   ->withHeader("Content-type", "application/json")
                   ->write($body);
    }
}

You must also configure Slim to use your custom error handler.

$container = $app->getContainer();

$container["errorHandler"] = function ($container) {
    return new Slim\Handlers\ApiError;
};

Check Slim API Skeleton for example implemention.

Mika Tuupola
  • 19,877
  • 5
  • 42
  • 49
  • Hey Mike. The one issue I have with this is sometimes the user is required to see the error messages (form validation, incorrect login etc), but any exceptions relating to database problems, or exceptions that haven't been caught for any reason I don't want those shown to the user and instead a generic message. This is why I wanted to separate the exceptions. Can this still be done this way? I'm guessing it could be done with the error code maybe. – BugHunterUK Dec 08 '16 at 16:15
  • 3
    You can do the same as with the middleware example you posted. When \App\Exceptions\AppException show detailed error. When something else so less detailed error. – Mika Tuupola Dec 08 '16 at 16:45
  • 1
    @BugHunterUK you can use Mika answer just fine, and simply check if the exception instance is `AppException` by using `if ($exception instanceOf AppException)`, and if so, send the exception JSON, otherwise, send a generic 'error occurred' JSON. – Ron Dadon Dec 13 '16 at 09:15