在PHP的魔术方法的上下文中,“ trigger_error”与“ throw Exception”


13

我正在与一位同事trigger_error魔术方法上下文中的正确用法(如果有)进行辩论。首先,我认为除了这种情况trigger_error应该避免这种情况。

说我们有一类的方法 foo()

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

现在说我们想提供完全相同的接口,但是使用魔术方法来捕获所有方法调用

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

这两个类的响应方式相同,foo()但在调用无效方法时有所不同。

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

我的观点是魔术方法应该trigger_error在捕获未知方法时调用

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;
        }
    }
}

这样两个类的行为(几乎)相同

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

我的用例是一个ActiveRecord实现。我使用__call来捕捉和处理方法,基本上做同样的事情,但有改性剂如DistinctIgnore

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

要么

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

类似的方法where()from()groupBy()等都是硬编码。

当您不小心打来电话时,我的论点突出了insret()。如果我的活动记录实现对所有方法都进行了硬编码,那将是一个错误。

与任何良好的抽象一样,用户应不了解实现细节,而应完全依赖于接口。为什么使用魔术方法的实现会有什么不同?两者都应该是一个错误。

Answers:


7

采取相同的ActiveRecord接口的两个实现(select()where()等)

class ActiveRecord1 {
    //Hardcodes all methods
}

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

如果在第一个类上调用了无效的方法,例如ActiveRecord1::insret(),默认的PHP行为是触发error。无效的函数/方法调用不是合理的应用程序想要捕获和处理的条件。当然,您可以使用Ruby或Python这样的语言来捕获它,但错误是一个例外,但是其他语言(JavaScript /任何静态语言/更多?)都会失败。

回到PHP-如果两个类都实现相同的接口,为什么它们不应该表现出相同的行为?

如果__call__callStatic检测到无效的方法,则它们应触发错误以模仿语言的默认行为

$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);

我不是在争论是否应该对异常使用错误(不应100%使用错误),但是我认为PHP的魔术方法是一个例外-双关语:)- 语言规则中的该规则


1
抱歉,我不买。为什么我们要成为绵羊,因为十年前首次在PHP中实现类时,PHP中不存在异常4.something
Matthew Scharley

@Matthew保持一致性。我不是在讨论异常与错误(没有争议),也不是PHP是否设计失败(确实如此),我是在这种非常独特的情况下,为了保持一致性,最好模仿这种语言的行为
chriso 2011年

一致性并不总是一件好事。一致但较差的设计仍然是较差的设计,最终很难使用。
Matthew Scharley

@Matthew是对的,但是IMO在无效方法调用上触发错误并不是一个糟糕的设计1)许多(大多数?)语言是内置的,以及2)我想不出您想要的任何一种情况要赶上一个无效的方法调用和处理呢?
chriso 2011年

@chriso:宇宙足够大,以至于有一些用例,您或我都不会梦想发生这种情况。您正在使用__call()动态路由,是否期望某人可能想处理失败的情况真的那么不合理吗?无论如何,这是一个圈子,所以这是我最后的评论。尽一切努力,最终归结为一个判断电话:更好的支持与一致性。在没有特殊处理的情况下,这两种方法都会对应用程序产生相同的效果。
马修·沙利

3

我将把我的见解抛在脑后,但是如果您trigger_error在任何地方使用,那么您在做错事。例外是解决之道。

例外的优点:

  • 他们可以被抓住。这是一个巨大的优势,应该是您唯一需要的优势。人们实际上可以尝试不同的尝试,如果他们期望有可能出现问题的话。错误不会给您这个机会。即使设置自定义错误处理程序,也无法简单地捕获异常。关于您在问题中的评论,什么是“合理的”应用程序完全取决于应用程序的上下文。如果人们认为例外情况永远不会发生,那么他们将无法捕获例外情况。不给别人选择是Bad Thing™。
  • 堆栈痕迹。如果出现问题,您会知道问题发生的地点背景。您是否曾经尝试找出某些核心方法中的错误出在哪里?如果调用的函数的参数太少,则会出现无用的错误,该错误会突出显示正在调用的方法的开始,并完全忽略了从何处进行调用。
  • 明晰。结合以上两者,您将获得更清晰的代码。如果您尝试使用自定义错误处理程序来处理错误(例如,生成错误的堆栈跟踪),则所有错误处理都在一个函数中,而不是实际生成错误的位置。

解决您的问题,调用不存在的方法可能是有可能的。这完全取决于您正在编写的代码的上下文,但是在某些情况下可能会发生这种情况。为了解决您的确切用例,某些数据库服务器可能允许某些其他服务器不提供的功能。在vs.函数中使用try/ catch和异常__call()来检查功能是完全不同的论点。

我能想到的唯一用例trigger_error是for E_USER_WARNING或更低。E_USER_ERROR在我看来,触发“ 尽管”始终是一个错误。


感谢您的答复:)-1)我同意例外应该几乎总是用于错误-您的所有论点都是有效的观点。但是..我认为,在这种情况下你的论点失败..
chriso

2
调用不存在的方法可能是有效的可能性 ”-我完全不同意。在每种语言(我曾经使用过)中,调用不存在的函数/方法都将导致错误。这不是应该抓住和处理的条件。静态语言不会让您使用无效的方法调用进行编译,而动态语言一旦到达则将失败。
chriso 2011年

如果您阅读了我的第二份编辑,您将看到我的用例。假设您有两个相同类的实现,一个使用__call,一个使用硬编码方法。忽略实现细节,为什么两个类在实现相同接口时的行为都不同?如果您使用具有硬编码方法的类调用无效的方法,PHP将触发错误trigger_error在__call或__callStatic上下文中使用可模仿该语言的默认行为
chriso 2011年

@chriso:不是PHP的动态语言将失败,并会捕获到异常。例如,Ruby抛出了一个NoMethodError您可以根据需要捕获的对象。就我个人而言,错误是PHP中的重大错误。仅仅因为内核使用了错误的报告错误的方法,并不意味着您自己的代码应该。
马修·沙利

@chriso我想相信核心仍然使用错误的唯一原因是为了向后兼容。希望PHP6将像PHP5一样是另一个飞跃,并且完全放弃错误。最终,错误和异常都会产生相同的结果:立即终止执行代码。除了一个例外,您可以诊断出为什么以及在何处容易得多。
马修·沙利

3

标准PHP错误应被视为过时的。PHP提供了一个内置类ErrorException,用于将错误,警告和通知转换为具有完整堆栈跟踪的异常。您可以这样使用它:

function errorToExceptionHandler($errNo, $errStr, $errFile, $errLine, $errContext)
{
if (error_reporting() == 0) return;
throw new ErrorException($errStr, 0, $errNo, $errFile, $errLine);
}
set_error_handler('errorToExceptionHandler');

使用这个,这个问题就变得毫无意义。内置错误现在引发异常,因此您自己的代码也应如此。


1
如果使用此方法,则需要检查错误的类型,以免将E_NOTICEs变成异常。那将是不好的。
马修·沙利

1
不,将E_NOTICES转换为异常很好!所有通知均应视为错误;无论您是否将它们转换为异常,这都是一个好习惯。
韦恩

2
通常,我会同意您的意见,但是第二次您开始使用任何第三方代码时,这种做法很快就会浮出水面。
马修·沙利

几乎所有的第三方库都是E_STRICT | E_ALL安全。如果我使用的不是,则我将对其进行修复。我已经以这种方式工作了好几年了
韦恩

您显然以前没有使用过Dual。这样的核心真的很好,但是很多第三方模块都不是
Matthew Scharley 2011年

0

海事组织,这是一个非常有效的用例trigger_error

function handleError($errno, $errstring, $errfile, $errline, $errcontext) {
    if (error_reporting() & $errno) {
        // only process when included in error_reporting
        return handleException(new \Exception($errstring, $errno));
    }
    return true;
}

function handleException($exception){
    // Here, you do whatever you want with the generated
    // exceptions. You can store them in a file or database,
    // output them in a debug section of your page or do
    // pretty much anything else with it, as if it's a
    // normal variable

    switch ($code) {
        case E_ERROR:
        case E_CORE_ERROR:
        case E_USER_ERROR:
            // Make sure script exits here
            exit(1);
        default:
            // Let script continue
            return true;
    }
}

// Set error handler to your custom handler
set_error_handler('handleError');
// Set exception handler to your custom handler
set_exception_handler('handleException');


// ---------------------------------- //

// Generate warning
trigger_error('This went wrong, but we can continue', E_USER_WARNING);

// Generate fatal error :
trigger_error('This went horrible wrong', E_USER_ERROR);

使用此策略,$errcontext如果$exception->getTrace()在函数中执行操作,则可以获取参数handleException。这对于某些调试目的非常有用。

不幸的是,这仅在trigger_error直接从上下文中使用时才有效,这意味着您不能使用包装函数/方法对该trigger_error函数进行别名(因此,function debug($code, $message) { return trigger_error($message, $code); }如果希望在跟踪中使用上下文数据,则无法执行类似操作)。

我一直在寻找更好的替代方法,但到目前为止我还没有找到任何替代方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.