Moving along through our detailed PHP Exception Handling series, today we'll take a closer look at the BadMethodCallException in PHP, along with its lesser-used parent BadFunctionCallException. The primary scenario in which a BadMethodCallException
is thrown is when calling either an instance or static method that doesn't exist, or expects a different argument signature than the one provided.
In this article we'll dig into the BadMethodCallException
a bit more, looking at where it resides in the PHP Exception Hierarchy. We'll also take a look at how BadMethodCallExceptions
should be used and handled in your own coding endeavors, so let's get movin'!
The Technical Rundown
- All PHP errors implement the
Throwable
interface, or are extended from another inherited class therein. Exception
implements theThrowable
interface.LogicException
extends theException
class.BadFunctionCallException
extends theLogicException
class.- Finally,
BadMethodCallException
extends theBadFunctionCallException
class.
Full Code Sample
Below is the full code sample we’ll be using in this article. Feel free to use any or all of the code if you wish to follow along.
<?php
/**
* Class Book
*/
class Book
{
private $author;
private $pageCount;
private $title;/**
* Magic method triggers when inaccessible instance method is invoked.
*
* Throws BadMethodCallException.
*
* @param string $name Name of invoked method.
* @param array $args Additional arguments.
*/
public function __call(string $name, array $args) {
throw new BadMethodCallException("Instance method Book->$name() doesn't exist");
}/**
* Magic method triggers when inaccessible static method is invoked.
*
* Throws BadMethodCallException.
*
* @param string $name Name of invoked method.
* @param array $args Additional arguments.
*/
public static function __callstatic(string $name, array $args) {
throw new BadMethodCallException("Static method Book::$name() doesn't exist");
}/**
* Book constructor.
*
* @param Book|string $title Book title.
* @param Book|string $author Book author.
* @param Book|int $pageCount Book page count.
*/
public function __construct(string $title, string $author, int $pageCount) {
$this->setAuthor($author);
$this->setPageCount($pageCount);
$this->setTitle($title);
}/**
* Get the author.
*
* @return string Book author.
*/
public function getAuthor(): string {
return $this->author;
}/**
* Set the author.
*
* @param string $value Author value to be set.
*/
public function setAuthor(string $value) {
$this->author = $value;
}/**
* Set the current page count of Book.
*
* @return mixed Page count of Book.
*/
public function getPageCount(): int {
return $this->pageCount;
}/**
* Get the current page count of Book.
*
* @param int $pageCount Page count to set.
*/
public function setPageCount(int $pageCount) {
$this->pageCount = $pageCount;
}/**
* Get the title.
*
* @return string Book title.
*/
public function getTitle(): string {
return $this->title;
}/**
* Set the title.
*
* @param string $value Title value to be set.
*/
public function setTitle(string $value) {
$this->title = $value;
}
}function executeExamples()
{
callInvalidInstanceMethod();
callInvalidStaticMethod();
callInvalidFunction();
}function callInvalidInstanceMethod() {
try {
// Create new Book instance.
$book = new Book("A Game of Thrones", "George R. R. Martin", 835);
// Call invalid method.
$book->checkout();
} catch (BadMethodCallException $exception) {
// Output expected BadMethodCallException.
Logging::Log($exception);
} catch (BadFunctionCallException $exception) {
// Output unexpected BadFunctionCallException.
Logging::Log($exception, false);
} catch (Exception $exception) {
// Output unexpected Exceptions.
Logging::Log($exception, false);
}
}function callInvalidStaticMethod() {
try {
// Call invalid static method.
Book::checkout();
} catch (BadMethodCallException $exception) {
// Output expected BadMethodCallException.
Logging::Log($exception);
} catch (BadFunctionCallException $exception) {
// Output unexpected BadFunctionCallException.
Logging::Log($exception, false);
} catch (Exception $exception) {
// Output unexpected Exceptions.
Logging::Log($exception, false);
}
}function callInvalidFunction() {
try {
// Call an invalid function.
invalidFunction();
} catch (BadMethodCallException $exception) {
// Output unexpected BadMethodCallException.
Logging::Log($exception, false);
} catch (BadFunctionCallException $exception) {
// Output expected BadFunctionCallException.
Logging::Log($exception);
} catch (Exception $exception) {
// Output unexpected Exceptions.
Logging::Log($exception, false);
} catch (Error $error) {
// Output unexpected Errors.
Logging::Log($error,false);
}
}
executeExamples();
<?php
// Logging.php
require('kint.php');/**
* Provides basic logging/output functionality.
*/
class Logging {/**
* Logs the passed object, string, or Throwable instance to the console.
*
* @param object|string $a Message or value to be logged.
* @param object|bool $b Secondary value, such as boolean for Throwables indicating if error was expected.
*/
public static function Log($a, $b = null) {
if (is_string($a) || is_numeric($a)) {
Logging::LogString($a);
} elseif ($a instanceof Throwable) {
Logging::LogThrowable($a, is_null($b) ? true : $b);
} else {
Logging::LogObject($a);
}
}/**
* Logs the passed object.
*
* @param object $object Object to be logged.
*
* @see https://github.com/kint-php/kint Kint tool used for structured outputs.
*/
private static function LogObject(object $object) {
Kint_Renderer_Cli::$force_utf8 = true;
Kint_Renderer_Text::$decorations = false;
Kint::dump($object);
}/**
* Logs the passed string value.
*
* @param string $value Value to be logged.
*/
private static function LogString(string $value) {
print_r("{$value}\n");
}/**
* Logs the passed Throwable object.
* Includes message, className if error was expected, and stack trace.
*
* Uses internal Reflection to retrieve protected/private properties.
*
* @param Throwable $throwable Throwable object to be output.
* @param bool $expected Indicates if error was expected or not.
*/
private static function LogThrowable(Throwable $throwable, bool $expected = true) {
$expected = $expected ? "EXPECTED" : "UNEXPECTED";
$message = substr($throwable->xdebug_message, 1);
// Output whether error was expected or not, the class name, the message, and stack trace.
print_r("[{$expected}] {$message}\n");
// Add line separator to keep it tidy.
self::LineSeparator();
}/**
* Outputs a dashed line separator with
* inserted text centered in the middle.
*
* @param array ...$args Insert, length, and separator character.
*/
public static function LineSeparator(...$args) {
$insert = empty($args[0]) ? "" : $args[0];
$length = empty($args[1]) ? 40 : $args[1];
$separator = empty($args[2]) ? '-' : $args[2];$output = $insert;
if (strlen($insert) == 0) {
$output = str_repeat($separator, $length);
} elseif (strlen($insert) < $length) {
// Update length based on insert length, less a space for margin.
$length -= (strlen($insert) + 2);
// Halve the length and floor left side.
$left = floor($length / 2);
$right = $left;
// If odd number, add dropped remainder to right side.
if ($length % 2 != 0) $right += 1;// Create separator strings.
$left = str_repeat($separator, $left);
$right = str_repeat($separator, $right);// Surround insert with separators.
$output = "{$left} {$insert} {$right}";
}
print_r("{$output}\n");
}
}
When Should You Use It?
We have three examples that aim to illustrate how BadMethodCallExceptions
and BadFunctionCallExceptions
might come up. We start with the Book
class, which contains a few property getters and setters, along with two PHP "magic methods": __call()
and __callstatic()
.
/**
* Class Book
*/
class Book
{
private $author;
private $pageCount;
private $title;/**
* Magic method triggers when inaccessible instance method is invoked.
*
* Throws BadMethodCallException.
*
* @param string $name Name of invoked method.
* @param array $args Additional arguments.
*/
public function __call(string $name, array $args) {
throw new BadMethodCallException("Instance method Book->$name() doesn't exist");
}/**
* Magic method triggers when inaccessible static method is invoked.
*
* Throws BadMethodCallException.
*
* @param string $name Name of invoked method.
* @param array $args Additional arguments.
*/
public static function __callstatic(string $name, array $args) {
throw new BadMethodCallException("Static method Book::$name() doesn't exist");
}/**
* Book constructor.
*
* @param Book|string $title Book title.
* @param Book|string $author Book author.
* @param Book|int $pageCount Book page count.
*/
public function __construct(string $title, string $author, int $pageCount) {
$this->setAuthor($author);
$this->setPageCount($pageCount);
$this->setTitle($title);
}/**
* Get the author.
*
* @return string Book author.
*/
public function getAuthor(): string {
return $this->author;
}/**
* Set the author.
*
* @param string $value Author value to be set.
*/
public function setAuthor(string $value) {
$this->author = $value;
}/**
* Set the current page count of Book.
*
* @return mixed Page count of Book.
*/
public function getPageCount(): int {
return $this->pageCount;
}/**
* Get the current page count of Book.
*
* @param int $pageCount Page count to set.
*/
public function setPageCount(int $pageCount) {
$this->pageCount = $pageCount;
}/**
* Get the title.
*
* @return string Book title.
*/
public function getTitle(): string {
return $this->title;
}
/**
* Set the title.
*
* @param string $value Title value to be set.
*/
public function setTitle(string $value) {
$this->title = $value;
}
}
The __call()
magic method triggers when an invalid instance method is called on a Book
class instance. Similarly, the __callstatic()
magic method is triggered when attempting to call an invalid static method. In both cases, the common technique is to throw
a new BadMethodCallException
indicating the problem. This ensures that a typo or incorrect method call is noticed immediately and can be remedied.
To see this in action, we start with the callInvalidInstanceMethod()
function:
function callInvalidInstanceMethod() {
try {
// Create new Book instance.
$book = new Book("A Game of Thrones", "George R. R. Martin", 835);
// Call invalid method.
$book->checkout();
} catch (BadMethodCallException $exception) {
// Output expected BadMethodCallException.
Logging::Log($exception);
} catch (BadFunctionCallException $exception) {
// Output unexpected BadFunctionCallException.
Logging::Log($exception, false);
} catch (Exception $exception) {
// Output unexpected Exceptions.
Logging::Log($exception, false);
}
}
All we're doing here is creating a new Book
instance and then trying to call the checkout()
method, which doesn't exist. This throws a BadMethodCallException
, as expected, which we catch and output:
[EXPECTED] BadMethodCallException: Instance method Book->checkout() doesn't exist in D:\work\Airbrake.io\Exceptions\PHP\Exception\LogicException\BadFunctionCallException\code.php on line 90
Not too surprisingly, we'll be using the callInvalidStaticMethod()
method to test the Book::__callstatic()
magic method:
function callInvalidStaticMethod() {
try {
// Call invalid static method.
Book::checkout();
} catch (BadMethodCallException $exception) {
// Output expected BadMethodCallException.
Logging::Log($exception);
} catch (BadFunctionCallException $exception) {
// Output unexpected BadFunctionCallException.
Logging::Log($exception, false);
} catch (Exception $exception) {
// Output unexpected Exceptions.
Logging::Log($exception, false);
}
}
We need very little logical code to properly test this one -- all we need to do is to try calling the missing static Book::checkout()
method. The output shows that our slightly different BadMethodCallException
was thrown, as intended:
[EXPECTED] BadMethodCallException: Static method Book::checkout() doesn't exist in D:\work\Airbrake.io\Exceptions\PHP\Exception\LogicException\BadFunctionCallException\code.php on line 102
Now we run into a bit of a strange situation. As we saw in the hierarchy section above, BadMethodCallException
inherits from the BadFunctionCallException
class. However, causing a BadFunctionCallException
is quite uncommon and is generally not considered appropriate in userland code. There are a few reasons for this, but the biggest reason is that PHP itself throws a fatal error when an attempt is made to call an invalid function. To illustrate, take a look at the callInvalidFunction()
function:
function callInvalidFunction() {
try {
// Call an invalid function.
invalidFunction();
} catch (BadMethodCallException $exception) {
// Output unexpected BadMethodCallException.
Logging::Log($exception, false);
} catch (BadFunctionCallException $exception) {
// Output expected BadFunctionCallException.
Logging::Log($exception);
} catch (Exception $exception) {
// Output unexpected Exceptions.
Logging::Log($exception, false);
} catch (Error $error) {
// Output unexpected Errors.
Logging::Log($error,false);
}
}
Here we attempt to call the invalidFunction()
function, which doesn't exist in this context. One might expect a BadFunctionCallException
to be thrown, but PHP doesn't handle undefined functions that way. Instead, notice we're also catching any Errors
at the end of our catch
block series. Sure enough, thanks to the ability to catch such errors in PHP 7+, the output shows that PHP threw an unexpected error because of our invalid function call:
[UNEXPECTED] Error: Call to undefined function invalidFunction() in D:\work\Airbrake.io\Exceptions\PHP\Exception\LogicException\BadFunctionCallException\code.php on line 150
While you could explicitly throw BadFunctionCallExceptions
in your own code when a function isn't properly invoked, with most modern object-oriented design patterns, the BadMethodCallException
will likely be more appropriate to your needs.
Check out the Airbrake-PHP library, designed to quickly and easily integrate into any PHP project, giving you and your team access to real-time error monitoring and reporting throughout your application's entire life cycle. With automatic, instantaneous error and exception notifications at your fingertips, you'll be constantly aware of your application's health, including any issues that may arise. Best of all, with Airbrake's robust web dashboard cataloging every error that occurs, you and your team can immediately dive into the exact details of what went wrong, making it easy to quickly recognize and resolve problems.