I think it's impossible to give a definitive answer here, because choices like this are personal preference.
Consider that what follows is my approach, and I have no presumption it is the right one.
What I can say for sure is that you should avoid your third option:
Just return null/false
This is bad under different aspect:
- return type consinstency
- makes functions harder to unit test
- force conditional check on return type (
if (! is_null($thing))...
) making code harder to read
I, more than often, use OOP to code plugins, and my object methods often throw exception when something goes wrong.
Doing that, I:
- accomplish return type consinstency
- make the code simple to unit test
- don't need conditional check on the returned type
However, throwing exceptions in a WordPress plugin, means that nothing will catch them, ending up in a fatal error that is absolutely not desirable, expecially in production.
To avoid this issue, I normally have a "main routine" located in main plugin file, that I wrap in a try
/ catch
block. This gives me the chance to catch the exception in production and prevent the fatal error.
A rough example of a class:
# myplugin/src/Foo.php
namespace MyPlugin;
class Foo {
/**
* @return bool
*/
public function doSomething() {
if ( ! get_option('my_plugin_everything_ok') ) {
throw new SomethingWentWrongException('Something went wrong.');
}
// stuff here...
return true;
}
}
and using it from main plugin file:
# myplugin/main-plugin-file.php
namespace MyPlugin;
function initialize() {
try {
$foo = new Foo();
$foo->doSomething();
} catch(SomethingWentWrongException $e) {
// on debug is better to notice when bad things happen
if (defined('WP_DEBUG') && WP_DEBUG) {
throw $e;
}
// on production just fire an action, making exception accessible e.g. for logging
do_action('my_plugin_error_shit_happened', $e);
}
}
add_action('wp_loaded', 'MyPlugin\\initialize');
Of course, in real world you may throw and catch different kinds of exception and behave differently according to the exception, but this should give you a direction.
Another option I often use (and you don't mentioned) is to return objects that contain a flag to verify if no error happen, but keeping the return type consistency.
This is a rough example of an object like that:
namespace MyPlugin;
class Options {
private $options = [];
private $ok = false;
public function __construct($key)
{
$options = is_string($key) ? get_option($key) : false;
if (is_array($options) && $options) {
$this->options = $options;
$this->ok = true;
}
}
public function isOk()
{
return $this->ok;
}
}
Now, from any place in your plugin, you can do:
/**
* @return MyPlugin\Options
*/
function my_plugin_get_options() {
return new MyPlugin\Options('my_plugin_options');
}
$options = my_plugin_get_options();
if ($options->isOk()) {
// do stuff
}
Note how my_plugin_get_options()
above always returns an instance of Options
class, in this way you can always pass the return value around, and even inject it to other objects that use type-hint with now worries that the type is different.
If the function had returned null
/ false
in case of error, before passing it around you had been forced to check if returned value is valid.
At same time, you have a clearly way to understand is something is wrong with the option instance.
This is a good solution in case the error is something that can be easily recovered, using defaults or whatever fits.