Search code examples
phpexceptionmethod-chaining

Which PHP exception to throw when last method in chain requires other method called first


I am writing a PHP query builder-style library that uses method chaining to add constraints to the query with a final call to get() at the end to execute the request, similar to Laravel's query builder, but using a public JSON API via Guzzle for the data. For example:

// valid
Foo::queryType()->take(5)->skip(50)->get();

// invalid, missing query type
Foo::take->(5)->get();

Foo is just a facade for accessing the library. queryType() is a required initial method (can be one of many, e.g. queryTypeA(), queryTypeB()) that sets a protected class variable in Foo. I want to throw an Exception if get() is called without that variable being set first. And I want to be precise in what gets thrown.

PHP's documentation lists a few options:

  • BadMethodCallException:

    Exception thrown if a callback refers to an undefined method or if some arguments are missing.

Probably doesn't refer to missing prerequisites, and I get the feeling this is intended more for dynamic calls where the method doesn't exist.

  • BadFunctionCallException

    Exception thrown if a callback refers to an undefined function or if some arguments are missing.

Basically same as before but for general functions, and BadMethodCallException is actually a child class of this.

While not technically an argument, an unset protected class variable required to continue execution fits the general idea, so this one makes the most sense so far.

  • UnexpectedValueException

    Exception thrown if a value does not match with a set of values. Typically this happens when a function calls another function and expects the return value to be of a certain type or value not including arithmetic or buffer related errors.

Not really an option; while the name implies that I might want the opposite of this - I expect a value to be set and it's not present - the description seems less of a match than others, and there's no user contributed notes to help refine.

  • RuntimeException

    Exception thrown if an error which can only be found on runtime occurs.

My fallback. Maybe I'm overthinking this? Which PHP exception makes the most sense?


Solution

  • Based on the design of my library, I ended up going with BadMethodCallException. Since the outer class was simply a facade, I used the __call magic method to pass through any undefined method calls to the internal class variable.

    After passing through the call, the internal class variable is set to null so as to literally undefine the method call. However so as to distinguish between undefined methods and out of order chaining, I explicitly check the class variable for null and provide the specific error.

    protected $request = null;
    
    /**
     * Pass through non-explicitly defined method calls to the internal request class
     * @param  string $function
     * @param  array $args
     * @return mixed
     * @throws \BadMethodCallException
     */
    public function __call($function, $args)
    {
        if ($this->request === null) {
            throw new \BadMethodCallException('Must specify request type before chaining constraints.');
        }
    
        if (method_exists($this->request, $function)) {
            $result = $this->request->$function(sizeof($args) ? $args[0] : null);
    
            $this->request = null;
    
            return $result;
        }
    
        throw new \BadMethodCallException('Method \'' . $function . '()\' does not exist.');
    }