Php – `trigger_error` vs `throw Exception` in the context of PHP’s magic methods

error handlingPHP

I'm having a debate with a colleague over the correct usage (if any) of trigger_error in the context of magic methods. Firstly, I think that trigger_error should be avoided except for this one case.

Say we have a class with one method foo()

class A {
    public function foo() {
        echo 'bar';
    }
}

Now say we want to provide the exact same interface but use a magic method to catch all method calls

class B {
    public function __call($method, $args) {
        switch (strtolower($method)) {
        case 'foo':
            echo 'bar';
            break;
        }
    }
}

$a = new A;
$b = new B;

$a->foo(); //bar
$b->foo(); //bar

Both classes are the same in the way they respond to foo() but differ when calling an invalid method.

$a->doesntexist(); //Error
$b->doesntexist(); //Does nothing

My argument is that magic methods should call trigger_error when a unknown method is caught

class B {
    public function __call($method, $args) {
        switch (strtolower($method)) {
        case 'foo':
            echo 'bar';
            break;
        default:
            $class = get_class($this);
            $trace = debug_backtrace();
            $file = $trace[0]['file'];
            $line = $trace[0]['line'];
            trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);
            break;
        }
    }
}

So that both classes behave (almost) identically

$a->badMethod(); //Call to undefined method A::badMethod() in [..] on line 28
$b->badMethod(); //Call to undefined method B::badMethod() in [..] on line 32

My use-case is an ActiveRecord implementation. I use __call to catch and handle methods that essentially do the same thing but have modifiers such as Distinct or Ignore, e.g.

selectDistinct()
selectDistinctColumn($column, ..)
selectAll()
selectOne()
select()

or

insert()
replace()
insertIgnore()
replaceIgnore()

Methods like where(), from(), groupBy(), etc. are hard-coded.

My argument is highlighted when you accidentally call insret(). If my active record implementation hardcoded all of the methods then it would be an error.

As with any good abstraction, the user should be unaware of the implementation details and rely solely on the interface. Why should the implementation that uses magic methods behave any differently? Both should be an error.

Best Answer

Take two implementations of the same ActiveRecord interface (select(), where(), etc.)

class ActiveRecord1 {
    //Hardcodes all methods
}

class ActiveRecord2 {
    //Uses __call to handle some methods, hardcodes the rest
}

If you call an invalid method on the first class, e.g. ActiveRecord1::insret(), the default PHP behaviour is to trigger an error. An invalid function/method call is not a condition that a reasonable application would want to catch and handle. Sure, you can catch it in languages like Ruby or Python where an error is an exception, but others (JavaScript / any static language / more?) will fail.

Back to PHP - if both classes implement the same interface, why shouldn't they exhibit the same behaviour?

If __call or __callStatic detect an invalid method, they should trigger an error to mimic the default behaviour of the language

$class = get_class($this);
$trace = debug_backtrace();
$file = $trace[0]['file'];
$line = $trace[0]['line'];
trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);

I'm not arguing whether errors should be used over exceptions (they 100% shouldn't), however I believe that PHP's magic methods are an exception - pun intended :) - to this rule in the context of the language

Related Topic