diff --git a/src/Dibi/Helpers.php b/src/Dibi/Helpers.php index fc11828d..b6788a03 100644 --- a/src/Dibi/Helpers.php +++ b/src/Dibi/Helpers.php @@ -92,6 +92,26 @@ class DibiHelpers } + /** + * Finds the best suggestion. + * @return string|NULL + * @internal + */ + public static function getSuggestion(array $items, $value) + { + $best = NULL; + $min = (int) (strlen($value) / 4) + 2; + foreach ($items as $item) { + $item = is_object($item) ? $item->getName() : $item; + if (($len = levenshtein($item, $value)) > 0 && $len < $min) { + $min = $len; + $best = $item; + } + } + return $best; + } + + /** @internal */ public static function escape($driver, $value, $type) { diff --git a/src/Dibi/Strict.php b/src/Dibi/Strict.php index 145baac3..ae62f6dc 100644 --- a/src/Dibi/Strict.php +++ b/src/Dibi/Strict.php @@ -27,7 +27,9 @@ trait DibiStrict return call_user_func_array($cb, $args); } $class = method_exists($this, $name) ? 'parent' : get_class($this); - throw new LogicException("Call to undefined method $class::$name()."); + $items = (new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC); + $hint = ($t = DibiHelpers::getSuggestion($items, $name)) ? ", did you mean $t()?" : '.'; + throw new LogicException("Call to undefined method $class::$name()$hint"); } @@ -37,8 +39,10 @@ trait DibiStrict */ public static function __callStatic($name, $args) { - $class = get_called_class(); - throw new LogicException("Call to undefined static method $class::$name()."); + $rc = new ReflectionClass(get_called_class()); + $items = array_intersect($rc->getMethods(ReflectionMethod::IS_PUBLIC), $rc->getMethods(ReflectionMethod::IS_STATIC)); + $hint = ($t = DibiHelpers::getSuggestion($items, $name)) ? ", did you mean $t()?" : '.'; + throw new LogicException("Call to undefined static method {$rc->getName()}::$name()$hint"); } @@ -54,8 +58,10 @@ trait DibiStrict $ret = $this->$m(); return $ret; } - $class = get_class($this); - throw new LogicException("Attempt to read undeclared property $class::$$name."); + $rc = new ReflectionClass($this); + $items = array_diff($rc->getProperties(ReflectionProperty::IS_PUBLIC), $rc->getProperties(ReflectionProperty::IS_STATIC)); + $hint = ($t = DibiHelpers::getSuggestion($items, $name)) ? ", did you mean $$t?" : '.'; + throw new LogicException("Attempt to read undeclared property {$rc->getName()}::$$name$hint"); } @@ -65,8 +71,10 @@ trait DibiStrict */ public function __set($name, $value) { - $class = get_class($this); - throw new LogicException("Attempt to write to undeclared property $class::$$name."); + $rc = new ReflectionClass($this); + $items = array_diff($rc->getProperties(ReflectionProperty::IS_PUBLIC), $rc->getProperties(ReflectionProperty::IS_STATIC)); + $hint = ($t = DibiHelpers::getSuggestion($items, $name)) ? ", did you mean $$t?" : '.'; + throw new LogicException("Attempt to write to undeclared property {$rc->getName()}::$$name$hint"); } diff --git a/tests/dibi/Strict.phpt b/tests/dibi/Strict.phpt index 7460ffb5..2cd47e55 100644 --- a/tests/dibi/Strict.phpt +++ b/tests/dibi/Strict.phpt @@ -9,6 +9,24 @@ class TestClass { use DibiStrict; + public $public; + + protected $protected; + + public static $publicStatic; + + public function publicMethod() + {} + + public static function publicMethodStatic() + {} + + protected function protectedMethod() + {} + + protected static function protectedMethodS() + {} + public function getBar() { return 123; @@ -44,6 +62,21 @@ Assert::exception(function () { $obj->callParent(); }, 'LogicException', 'Call to undefined method parent::callParent().'); +Assert::exception(function () { + $obj = new TestClass; + $obj->publicMethodX(); +}, 'LogicException', 'Call to undefined method TestClass::publicMethodX(), did you mean publicMethod()?'); + +Assert::exception(function () { // suggest static method + $obj = new TestClass; + $obj->publicMethodStaticX(); +}, 'LogicException', 'Call to undefined method TestClass::publicMethodStaticX(), did you mean publicMethodStatic()?'); + +Assert::exception(function () { // suggest only public method + $obj = new TestClass; + $obj->protectedMethodX(); +}, 'LogicException', 'Call to undefined method TestClass::protectedMethodX().'); + // writing Assert::exception(function () { @@ -51,6 +84,21 @@ Assert::exception(function () { $obj->undeclared = 'value'; }, 'LogicException', 'Attempt to write to undeclared property TestClass::$undeclared.'); +Assert::exception(function () { + $obj = new TestClass; + $obj->publicX = 'value'; +}, 'LogicException', 'Attempt to write to undeclared property TestClass::$publicX, did you mean $public?'); + +Assert::exception(function () { // suggest only non-static property + $obj = new TestClass; + $obj->publicStaticX = 'value'; +}, 'LogicException', 'Attempt to write to undeclared property TestClass::$publicStaticX.'); + +Assert::exception(function () { // suggest only public property + $obj = new TestClass; + $obj->protectedX = 'value'; +}, 'LogicException', 'Attempt to write to undeclared property TestClass::$protectedX.'); + // property getter $obj = new TestClass; @@ -66,6 +114,21 @@ Assert::exception(function () { $val = $obj->undeclared; }, 'LogicException', 'Attempt to read undeclared property TestClass::$undeclared.'); +Assert::exception(function () { + $obj = new TestClass; + $val = $obj->publicX; +}, 'LogicException', 'Attempt to read undeclared property TestClass::$publicX, did you mean $public?'); + +Assert::exception(function () { // suggest only non-static property + $obj = new TestClass; + $val = $obj->publicStaticX; +}, 'LogicException', 'Attempt to read undeclared property TestClass::$publicStaticX.'); + +Assert::exception(function () { // suggest only public property + $obj = new TestClass; + $val = $obj->protectedX; +}, 'LogicException', 'Attempt to read undeclared property TestClass::$protectedX.'); + // unset/isset Assert::exception(function () {