2

I'm building a REST API using Slim Framework, on PHP 7.0 (Ubuntu 16.04). For proper exception handling, I extended the base \Exception class as follows :

namespace App\Exceptions;

class AppException extends \Exception
{

}

Then I use this as a base exception for all the application exceptions. For all exceptions that have a JSON response to be given to the user, I wrote another class :

namespace App\Exceptions;


use Slim\Container;
use Slim\Http\Request;
use Slim\Http\Response;

class JsonApiException extends AppException
{

    private $params = [
        "message" => "",
        "code" => 0,
        "previous" => null,
        "api" => [
            "message" => "",
            "code" => "",
            "status" => 200
        ]
    ];

    public function __construct(array $params)
    {
        $this->params = array_merge_recursive($this->params, $params);
        parent::__construct($this->params["message"], 0, $this->params["previous"]);
    }

    public function getApiMessage() {
        return $this->params["api"]["message"];
    }

    public function getApiCode() {
        return $this->params["api"]["code"];
    }

    public function getHttpStatusCode() {
        return $this->params["api"]["status"];
    }

    public function shouldBeLogged() {
        return false;
    }

    public function log(Container $container, Request $request) {
        if(!$this->shouldBeLogged()) return;
        $logger = $container->get('logger.info');
        $logger->info($this, array_merge($request->getHeaders(), [
            "method" => $_SERVER["REQUEST_METHOD"],
            "time" => $_SERVER["REQUEST_TIME"],
            "query_string" => $_SERVER["QUERY_STRING"],
            "host" => $_SERVER["HTTP_HOST"],
            "referer" => $_SERVER["HTTP_REFERER"],
            "user_agent" => $_SERVER["HTTP_USER_AGENT"],
            "ip" => $_SERVER["REMOTE_ADDR"],
            "uri" => $_SERVER["REQUEST_URI"]
        ]));
    }

    public function httpRespond(Response $response) {
        return $response->withJson([
            "error" => true,
            "errors" => [
                "server" => [[
                    "code" => $this->getApiCode(),
                    "message" => $this->getApiMessage()
                ]]
            ]
        ], $this->getHttpStatusCode());
    }

}

Then I use this as a base exception for all JSON errors. I'm using the following error to let the client know that the e-mail address it provided for registration already exists.

namespace App\Exceptions\Validation\User;

use App\Exceptions\JsonApiException;

class EmailAlreadyUsedException extends JsonApiException
{

    public function __construct()
    {
        parent::__construct([
            "message" => "The e-mail provided by the exception has already been used",
            "api" => [
                "message" => "The provided e-mail address has already been used",
                "code" => "EmailAlreadyUsed"
            ],
            "previous" => null
        ]);
    }

}

Whenever the error occurs, I add it to another custom exception to enable responding with multiple errors at once during validation :

namespace App\Exceptions;


use Slim\Http\Response;

class JsonApiMultipleException extends JsonApiException
{

    private $httpStatusCode = 200;
    private $exceptions = [];

    public function __construct($httpStatusCode = 200, \Exception $previous = null)
    {
        parent::__construct([]);
        $this->httpStatusCode = 200;
    }

    public function setHttpStatusCode(int $code) {
        $this->httpStatusCode = $code;
    }

    public function add(string $param, JsonApiException $exception) {
        if(!array_key_exists($param, $this->exceptions)) {
            $this->exceptions[$param] = [];
        }
        $this->exceptions[$param][] = $exception;
    }

    public function length() {
        $len = 0;
        foreach ($this->exceptions as $param => $exceptions) {
            $len += count($exceptions);
        }
        return $len;
    }

    public function map() {
        $mapped = [];
        foreach ($this->exceptions as $param => $exceptions) {
            $mapped[$param] = array_map(function (JsonApiException $exception) {
                return [
                    "code" => $exception->getApiCode(),
                    "message" => $exception->getApiMessage()
                ];
            }, $exceptions);
        }
        return $mapped;
    }

    public function getExceptions() {
        return $this->exceptions;
    }

    public function httpRespond(Response $response)
    {
        return $response->withJson([
            "error" => true,
            "errors" => $this->map()
        ], $this->httpStatusCode);
    }

}

But when I throw this exception during validation () :

namespace App\Validators;

use App\Exceptions\Validation\User\EmailAlreadyUsedException;
use App\Exceptions\Validation\User\InvalidEmailException;
use App\Exceptions\Validation\User\InvalidFirstNameException;
use App\Exceptions\Validation\User\InvalidLastNameException;
use App\Exceptions\Validation\User\InvalidPasswordException;
use Respect\Validation\Validator as v;

class UserValidator extends Validator
{

    //...

    private function validateEmail() {

        //Validate e-mail
        if(!v::stringType()->email()->validate($this->user->getEmail())) {
            $this->exception->add('email', new InvalidEmailException());
        }

        //Check if e-mail already used
        if(\UserQuery::create()->filterByEmail($this->user->getEmail())->count() > 0) {
            $this->exception->add('email', new EmailAlreadyUsedException());
        }
    }

    //...

}

It throws the following exception :

[Thu Jun 30 05:42:47 2016] Slim Application Error:
Type: Error
Message: Wrong parameters for App\Exceptions\Validation\User\EmailAlreadyUsedException([string $message [, long $code [, Throwable $previous = NULL]]])
File: /var/www/ElectroAbhi/app/Exceptions/JsonApiException.php
Line: 33
Trace: #0 /var/www/ElectroAbhi/app/Exceptions/JsonApiException.php(33): Exception->__construct(Array, 0, Array)
#1 /var/www/ElectroAbhi/app/Exceptions/Validation/User/EmailAlreadyUsedException.php(19): App\Exceptions\JsonApiException->__construct(Array)
#2 /var/www/ElectroAbhi/app/Validators/UserValidator.php(58): App\Exceptions\Validation\User\EmailAlreadyUsedException->__construct()
#3 /var/www/ElectroAbhi/app/Validators/UserValidator.php(32): App\Validators\UserValidator->validateEmail()
#4 /var/www/ElectroAbhi/app/Routes/API/User/Create.php(39): App\Validators\UserValidator->validate()
#5 [internal function]: App\Routes\API\User\Create->__invoke(Object(Slim\Http\Request), Object(Slim\Http\Response), Array)
#6 /var/www/ElectroAbhi/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php(41): call_user_func(Object(App\Routes\API\User\Create), Object(Slim\Http\Request), Object(Slim\Http\Response), Array)
#7 /var/www/ElectroAbhi/vendor/slim/slim/Slim/Route.php(325): Slim\Handlers\Strategies\RequestResponse->__invoke(Object(App\Routes\API\User\Create), Object(Slim\Http\Request), Object(Slim\Http\Response), Array)
#8 /var/www/ElectroAbhi/vendor/slim/slim/Slim/MiddlewareAwareTrait.php(116): Slim\Route->__invoke(Object(Slim\Http\Request), Object(Slim\Http\Response))
#9 /var/www/ElectroAbhi/vendor/slim/slim/Slim/Route.php(297): Slim\Route->callMiddlewareStack(Object(Slim\Http\Request), Object(Slim\Http\Response))
#10 /var/www/ElectroAbhi/vendor/slim/slim/Slim/App.php(443): Slim\Route->run(Object(Slim\Http\Request), Object(Slim\Http\Response))
#11 /var/www/ElectroAbhi/vendor/slim/slim/Slim/MiddlewareAwareTrait.php(116): Slim\App->__invoke(Object(Slim\Http\Request), Object(Slim\Http\Response))
#12 /var/www/ElectroAbhi/vendor/slim/slim/Slim/App.php(337): Slim\App->callMiddlewareStack(Object(Slim\Http\Request), Object(Slim\Http\Response))
#13 /var/www/ElectroAbhi/vendor/slim/slim/Slim/App.php(298): Slim\App->process(Object(Slim\Http\Request), Object(Slim\Http\Response))
#14 /var/www/ElectroAbhi/public/index.php(105): Slim\App->run()
#15 {main}

I'm really confused here, how is it showing the definition as EmailAlreadyUsedException([string $message [, long $code [, Throwable $previous = NULL]]]), when I've clearly implemented the constructor in EmailAlreadyUsedException, which takes no parameters?

UPDATE

I tried to debug and find out why $this->params["message"] is an array in the constructor of JsonApiException, but now I'm even more confused :

class JsonApiException extends AppException
{

    private $params = [
        "message" => "",
        "code" => 0,
        "previous" => null,
        "api" => [
            "message" => "",
            "code" => "",
            "status" => 200
        ]
    ];

    public function __construct(array $params)
    {
        print_r($params);
        die;
        $this->params = array_merge_recursive($this->params, $params);
        parent::__construct($this->params["message"], 0, $this->params["previous"]);
    }

}

Result -> Array ()

Even though I'm passing

parent::__construct([
            "message" => "The e-mail provided by the exception has already been used",
            "api" => [
                "message" => "The provided e-mail address has already been used",
                "code" => "EmailAlreadyUsed"
            ],
            "previous" => null
        ]);

from EmailAlreadyUsedException to the constructor of JsonApiException, the $params array seems to arrive empty. Am I missing something again?

2 Answers 2

3

All of your exception classes call parent::__construct. This means, ultimately, the \Exception class constructor will get called. You are not providing the correct parameters for that constructor.

You can tell from your stacktrace:

Trace: #0 /var/www/ElectroAbhi/app/Exceptions/JsonApiException.php(33): Exception->__construct(Array, 0, Array)

Your EmailAlreadyUsedException invokes the constructor of the JsonApiException, which invokes the constructor of PHP's native \Exception with (Array, 0, Array), which is not what the constructor expects.

You have to fix these two lines:

 $this->params = array_merge_recursive($this->params, $params);
 parent::__construct($this->params["message"], 0, $this->params["previous"]);

Apparently, $this->params["message"] and $this->params["previous"] are arrays. But the params you pass to parent::__construct must match the signature

[string $message [, long $code [, Throwable $previous = NULL]]]

Overriding a constructor in a derived class does not override the constructor in a parent class.

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

5 Comments

But I'm passing the correct parameters to the constructor of \Exception from JsonApiException? Overriding a constructor allows you to change it's definition, doesn't it?
@abishek The stack trace says you are passing an array, a number and an array, which is not what the signature of the parent constructor expects. Overriding a constructor in a derived class does not override the constructor in a parent class.
I tried to debug why $this->params["message"] is an array in the constructor and got totally new results. Can you please check my update?
@abhishek I cannot reproduce the print_r($params) to give an empty array. Don't know why you are getting this but the code you show in your question will not produce that.
The issue is solved, please see my answer. But yes, it's weird, print_r($params) still gives me an empty array, even though the array_replace_recursive is taking the correct values.
0

After long debugging sessions, the fault was not in the OOP behavior. It was in the function I was using to merge default parameters in JsonApiException. Instead of using array_merge_recursive which merges the elements of one or more arrays together so that the values of one are appended to the end of the previous one, I used array_replace_recursive.

When using array_merge_recursive, the $params property of the JsonApiException got assigned the value :

Array
(
    [message] => Array
        (
            [0] =>
            [1] => The e-mail provided by the exception has already been used
        )

    [code] => Array
        (
            [0] => 0
            [1] => 0
        )

    [api] => Array
        (
            [message] => Array
                (
                    [0] =>
                    [1] => The provided e-mail address has already been used
                )

            [code] => Array
                (
                    [0] =>
                    [1] => EmailAlreadyUsed
                )

            [status] => Array
                (
                    [0] => 200
                    [1] => 
                )

        )

)

Whereas, when using array_replace_recursive, the $params property becomes :

Array
(
    [message] => The e-mail provided by the exception has already been used
    [code] => 0
    [previous] =>
    [api] => Array
        (
            [message] => The provided e-mail address has already been used
            [code] => EmailAlreadyUsed
            [status] => 200
        )

)

Which in turn provided correct parameters to \Exception::__construct().

Just one thing is weird though, although it works, when I try to do this :

class JsonApiException extends AppException
{

    private $params = [
        "message" => "",
        "code" => 0,
        "previous" => null,
        "api" => [
            "message" => "",
            "code" => "",
            "status" => 200
        ]
    ];

    public function __construct(array $params)
    {
        print_r($params);
        die;
        $this->params = array_replace_recursive($this->params, $params);
        parent::__construct($this->params["message"], 0, $this->params["previous"]);
    }

}

I'm still getting Array (), which is weird because array_replace_recursive sets the corrects values.

Comments

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.