Page MenuHomePhabricator

Don't throw `PhutilMissingSymbolException` from `class_exists` or `interface_exists`
ClosedPublic

Authored by joshuaspence on Nov 19 2015, 7:27 PM.
Tags
None
Referenced Files
F14012417: D14523.id35315.diff
Fri, Nov 1, 12:13 PM
F14006067: D14523.id35244.diff
Mon, Oct 28, 12:32 AM
F13999242: D14523.id35315.diff
Thu, Oct 24, 1:38 PM
F13997020: D14523.id35317.diff
Thu, Oct 24, 1:42 AM
F13989279: D14523.diff
Mon, Oct 21, 7:27 PM
F13986740: D14523.id35126.diff
Mon, Oct 21, 5:11 AM
F13978843: D14523.diff
Sat, Oct 19, 1:16 AM
F13975851: D14523.diff
Fri, Oct 18, 11:53 AM
Subscribers

Details

Summary

Fixes T1116. Currently we throw a PhutilMissingSymbolException from the libphutil autoloader function (__phutil_autoload) if the following criteria are met:

  • The class/interface being loaded does not exist.
  • __phutil_autoload is the last registered autoloader.

This causes problems with some third-party code which does class_exists($some_class) where $some_class doesn't exist. This function causes the autoloader to be invoked, which thus throws PhutilMissingSymbolException. This is clearly undesirable and, as such, a rather hacky fix is to inspect the output of debug_backtrace() and look for a call to class_exists, interface_exists or trait_exists anywhere in the stacktrace. If any of these functions are found in the stacktrace, we simply return false instead of throwing a PhutilMissingSymbolException.

Test Plan

Ran ./scripts_update_compat_info.php without error.

Diff Detail

Repository
rPHU libphutil
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

joshuaspence retitled this revision from to Add a dummy autoload function to work around T1116..
joshuaspence updated this object.
joshuaspence edited the test plan for this revision. (Show Details)
joshuaspence added a reviewer: epriestley.
joshuaspence retitled this revision from Add a dummy autoload function to work around T1116. to Add a dummy autoload function to work around autoloader issue.Nov 19 2015, 7:36 PM
joshuaspence updated this object.
joshuaspence edited edge metadata.
epriestley edited edge metadata.

What do you think about this approach instead?

  • Before throwing a "not found" exception, check if class_exists() is on the callstack.
  • (If it is, check if any other autoloaders are registered? I think it's fine to just proceed even in a pure libphutil environment.)
  • If they are (even if we are the last autoloader), return false / no-op instead of throwing.

That is, here and in T1116 the specific problem behavior is that callers invoke class_exists('x') and expect it to return false. The behavior of php-parser, at least, is not really correct: it should pass false, I think, to disable autoloading, or just not do the check at all because the include has no side effects, I think. But we're broadly powerless to change that since we can't send patches to every project ever, this usage is common, and some of them probably aren't strictly incorrect.

This revision now requires changes to proceed.Nov 23 2015, 3:28 PM
joshuaspence edited edge metadata.

Check for class_exists

joshuaspence retitled this revision from Add a dummy autoload function to work around autoloader issue to Don't throw `PhutilMissingSymbolException` from `class_exists` or `interface_exists`.Nov 24 2015, 9:34 PM
joshuaspence updated this object.
joshuaspence edited edge metadata.
epriestley edited edge metadata.

Cool. This definitely feels little hacky, but I think we're ultimately working around somewhat-questionable behavior in third-party libraries in every case anyway and this may actually get us the result we desire without needing any weird dummy autoload methods anywhere or any special behaviors or an alternate include file or whatever.

src/__phutil_library_init__.php
38

This might need an:

if (empty($backtrace['function'])) {
  continue;
}

...first -- I'm not sure every stack frame is guaranteed to have the function key populated. I think I've seen some weird frames coming out of, e.g., callbacks from extensions like preg_replace_callback() and cURL, but might be misremembering.

(We should avoid using idx() here because it might not have loaded yet.)

This revision is now accepted and ready to land.Nov 25 2015, 3:01 PM

For what it's worth, I don't think that there is anything wrong with the third-party code doing class_exists($x) instead of class_exists($x, false) because the two are not equivalent. I would expect the latter to always return false unless the class $x had previously been autoloaded.

For example:

<?php

require_once 'scripts/__init_script__.php';

class_exists('SomePhutilClass', false); // I think this should always return false
joshuaspence edited edge metadata.

Check for empty

Don't throw from trait_exists

This revision was automatically updated to reflect the committed changes.

The two aren't equivalent, but I imagine most callers may really mean to ask "Has this class already been loaded?", not "Does this class exist somewhere?".

The call in T8494 seems to be asking the former question, and could probably be written more precisely as class_exists(X, false) (but better still would be omitting the check entirely, as it is is meaningless, unless the library legitimately expects callers to substitute a different PhpParser\Autoloader by loading it first, in which case it should not do that).

There are probably some callers asking the second question, but I'd venture a guess that the first question is a lot more common. I think the original T1116 report was something similar to this one, as well.