diff --git a/src/Scream.php b/src/Scream.php new file mode 100644 index 0000000..dd1d572 --- /dev/null +++ b/src/Scream.php @@ -0,0 +1,118 @@ + + * @author David Grudl + */ +trait Scream +{ + + /** + * Call to undefined method. + * + * @param string $name method name + * @param array $args arguments + * @throws \Kdyby\StrictObjects\MemberAccessException + */ + public function __call($name, $args) + { + $class = get_class($this); + $hint = Suggester::suggestMethod($class, $name); + throw new MemberAccessException("Call to undefined method $class::$name()" . ($hint ? ", did you mean $hint()?" : '.')); + } + + + + /** + * Call to undefined static method. + * + * @param string $name method name (in lower case!) + * @param array $args arguments + * @throws \Kdyby\StrictObjects\MemberAccessException + */ + public static function __callStatic($name, $args) + { + $class = get_called_class(); + $hint = Suggester::suggestStaticFunction($class, $name); + throw new MemberAccessException("Call to undefined static method $class::$name()" . ($hint ? ", did you mean $hint()?" : '.')); + } + + + + /** + * Returns property value. Do not call directly. + * + * @param string $name property name + * @throws \Kdyby\StrictObjects\MemberAccessException + */ + public function &__get($name) + { + $class = get_class($this); + $hint = Suggester::suggestProperty($class, $name); + throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); + } + + + + /** + * Sets value of a property. Do not call directly. + * + * @param string $name property name + * @param mixed $value property value + * @throws \Kdyby\StrictObjects\MemberAccessException + * @return void + */ + public function __set($name, $value) + { + $class = get_class($this); + $hint = Suggester::suggestProperty($class, $name); + throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); + } + + + + /** + * Is property defined? + * + * @param string $name property name + * @throws \Kdyby\StrictObjects\MemberAccessException + * @return bool + */ + public function __isset($name) + { + $class = get_class($this); + $hint = Suggester::suggestProperty($class, $name); + throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); + } + + + + /** + * Access to undeclared property. + * + * @param string $name property name + * @throws \Kdyby\StrictObjects\MemberAccessException + * @return void + */ + public function __unset($name) + { + $class = get_class($this); + throw new MemberAccessException("Cannot unset the property $class::\$$name."); + } + +} diff --git a/src/Suggester.php b/src/Suggester.php new file mode 100644 index 0000000..61adad4 --- /dev/null +++ b/src/Suggester.php @@ -0,0 +1,100 @@ + + */ +final class Suggester +{ + + /** + * @param string $class + * @param string $method + * @return NULL|string + */ + public static function suggestMethod($class, $method) + { + return self::getSuggestion( + get_class_methods($class), + $method + ); + } + + + + /** + * @param string $class + * @param string $method + * @return NULL|string + */ + public static function suggestStaticFunction($class, $method) + { + return self::getSuggestion( + array_filter( + get_class_methods($class), + function ($m) use ($class) { + return (new \ReflectionMethod($class, $m))->isStatic(); + } + ), + $method + ); + } + + + + /** + * @param string $class + * @param string $name + * @return NULL|string + */ + public static function suggestProperty($class, $name) + { + return self::getSuggestion( + array_keys(get_class_vars($class)), + $name + ); + } + + + + /** + * Finds the best suggestion (for 8-bit encoding). + * + * @author David Grudl (https://davidgrudl.com) + * @license See https://nette.org/en/license + * @return string|NULL + * @internal + */ + public static function getSuggestion(array $items, $value) + { + $norm = preg_replace($re = '#^(get|set|has|is|add)(?=[A-Z])#', '', $value); + $best = NULL; + $min = (strlen($value) / 4 + 1) * 10 + .1; + foreach (array_unique($items) as $item) { + if ($item !== $value && ( + ($len = levenshtein($item, $value, 10, 11, 10)) < $min + || ($len = levenshtein(preg_replace($re, '', $item), $norm, 10, 11, 10) + 20) < $min + ) + ) { + $min = $len; + $best = $item; + } + } + return $best; + } + +} diff --git a/src/exceptions.php b/src/exceptions.php new file mode 100644 index 0000000..647bb58 --- /dev/null +++ b/src/exceptions.php @@ -0,0 +1,23 @@ + + */ +class ScreamTest extends Tester\TestCase +{ + + public function testMagicCall() + { + $o = new SomeObject(); + + Assert::exception(function () use ($o) { + $o->someBaZ(); + }, 'Kdyby\StrictObjects\MemberAccessException', 'Call to undefined method KdybyTests\StrictObjects\SomeObject::someBaZ(), did you mean someBar()?'); + } + + + + public function testMagicStaticCall() + { + Assert::exception(function () { + SomeObject::staBaz(); + }, 'Kdyby\StrictObjects\MemberAccessException', 'Call to undefined static method KdybyTests\StrictObjects\SomeObject::staBaz(), did you mean staBar()?'); + } + + + + public function testMagicGet() + { + $o = new SomeObject(); + + Assert::exception(function () use ($o) { + $o->baz; + }, 'Kdyby\StrictObjects\MemberAccessException', 'Cannot read an undeclared property KdybyTests\StrictObjects\SomeObject::$baz, did you mean $bar?'); + } + + + + public function testMagicSet() + { + $o = new SomeObject(); + + Assert::exception(function () use ($o) { + $o->baz = 'value'; + }, 'Kdyby\StrictObjects\MemberAccessException', 'Cannot write to an undeclared property KdybyTests\StrictObjects\SomeObject::$baz, did you mean $bar?'); + } + + + + public function testMagicUnset() + { + $o = new SomeObject(); + + Assert::exception(function () use ($o) { + unset($o->baz); + }, 'Kdyby\StrictObjects\MemberAccessException', 'Cannot unset the property KdybyTests\StrictObjects\SomeObject::$baz.'); + } + + + + public function testMagicIsset() + { + $o = new SomeObject(); + + Assert::exception(function () use ($o) { + isset($o->baz); + }, 'Kdyby\StrictObjects\MemberAccessException', 'Cannot write to an undeclared property KdybyTests\StrictObjects\SomeObject::$baz, did you mean $bar?'); + } + +} + + + +(new ScreamTest())->run(); diff --git a/tests/Suggester.phpt b/tests/Suggester.phpt new file mode 100644 index 0000000..5188a3a --- /dev/null +++ b/tests/Suggester.phpt @@ -0,0 +1,73 @@ + + */ +class SuggesterTest extends Tester\TestCase +{ + + /** + * length allowed ins/del replacements + * ------------------------------------- + * 0 1 0 + * 1 1 1 + * 2 1 1 + * 3 1 1 + * 4 2 1 + * 5 2 2 + * 6 2 2 + * 7 2 2 + * 8 3 2 + */ + public function dataSuggestion() + { + return [ + [NULL, [], ''], + [NULL, [], 'a'], + [NULL, ['a'], 'a'], + ['a', ['a', 'b'], ''], + ['b', ['a', 'b'], 'a'], // ignore 100% match + ['a1', ['a1', 'a2'], 'a'], // take first + [NULL, ['aaa', 'bbb'], 'a'], + [NULL, ['aaa', 'bbb'], 'ab'], + [NULL, ['aaa', 'bbb'], 'abc'], + ['bar', ['foo', 'bar', 'baz'], 'baz'], + ['abcd', ['abcd'], 'acbd'], + ['abcd', ['abcd'], 'axbd'], + [NULL, ['abcd'], 'axyd'], // 'tags' vs 'this' + [NULL, ['setItem'], 'item'], + ['setItem', ['setItem'], 'Item'], + ['setItem', ['setItem'], 'addItem'], + [NULL, ['addItem'], 'addItem'], + ]; + } + + + + /** + * @dataProvider dataSuggestion + */ + public function testGetSuggestion($expected, $items, $value) + { + Assert::same($expected, Suggester::getSuggestion($items, $value)); + } + +} + + + +(new SuggesterTest())->run(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 0000000..e0fc9ff --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,21 @@ + + */ +class SomeObject +{ + + use Scream; + + public $foo; + + public $bar; + + + + public function someBar() + { + + } + + + + public function someFoo() + { + + } + + + + public static function staFoo() + { + + } + + + + public static function staBar() + { + + } + +}