diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 90ef336b2..1611ee26d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -70,6 +70,11 @@ parameters: count: 1 path: src/Database/Model.php + - + message: "#^Static property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$dispatcher \\(Illuminate\\\\Contracts\\\\Events\\\\Dispatcher\\) in isset\\(\\) is not nullable\\.$#" + count: 1 + path: src/Database/Model.php + - message: "#^Call to an undefined method Winter\\\\Storm\\\\Database\\\\Model\\:\\:errors\\(\\)\\.$#" count: 1 @@ -710,21 +715,6 @@ parameters: count: 1 path: src/Database/TreeCollection.php - - - message: "#^Winter\\\\Storm\\\\Extension\\\\Extendable\\:\\:extendableCall\\(\\) calls parent\\:\\:__call\\(\\) but Winter\\\\Storm\\\\Extension\\\\Extendable does not extend any class\\.$#" - count: 1 - path: src/Extension/Extendable.php - - - - message: "#^Winter\\\\Storm\\\\Extension\\\\Extendable\\:\\:extendableGet\\(\\) calls parent\\:\\:__get\\(\\) but Winter\\\\Storm\\\\Extension\\\\Extendable does not extend any class\\.$#" - count: 1 - path: src/Extension/Extendable.php - - - - message: "#^Winter\\\\Storm\\\\Extension\\\\Extendable\\:\\:extendableSet\\(\\) calls parent\\:\\:__set\\(\\) but Winter\\\\Storm\\\\Extension\\\\Extendable does not extend any class\\.$#" - count: 1 - path: src/Extension/Extendable.php - - message: "#^Parameter \\#2 \\$data \\(array\\) of method Winter\\\\Storm\\\\Mail\\\\Mailer\\:\\:queue\\(\\) should be compatible with parameter \\$queue \\(string\\|null\\) of method Illuminate\\\\Contracts\\\\Mail\\\\MailQueue\\:\\:queue\\(\\)$#" count: 1 diff --git a/src/Database/Model.php b/src/Database/Model.php index 7e436569c..e96502dfe 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -1,6 +1,6 @@ extensionGetParentClass(); + if ($parent !== false && $this->extensionMethodExists($parent, '__get')) { + return $this->extensionCallMethod($parent, '__get', [$name]); } return null; @@ -413,9 +414,9 @@ public function extendableSet($name, $value) /* * This targets trait usage in particular */ - $parent = get_parent_class(); - if ($parent !== false && method_exists($parent, '__set')) { - parent::__set($name, $value); + $parent = $this->extensionGetParentClass(); + if ($parent !== false && $this->extensionMethodExists($parent, '__set')) { + $this->extensionCallMethod($parent, '__set', [$name, $value]); } /* @@ -457,9 +458,9 @@ public function extendableCall($name, $params = null) } } - $parent = get_parent_class(); - if ($parent !== false && method_exists($parent, '__call')) { - return parent::__call($name, $params); + $parent = $this->extensionGetParentClass(); + if ($parent !== false && $this->extensionMethodExists($parent, '__call')) { + return $this->extensionCallMethod($parent, '__call', [$name, $params]); } throw new BadMethodCallException(sprintf( @@ -550,4 +551,75 @@ protected function extensionGetClassLoader(): ?ClassLoader return self::$extendableClassLoader = App::make(ClassLoader::class); } + + /** + * Gets the parent class using reflection. + * + * The parent class must either not be the `Extendable` class, or must not be using the `ExtendableTrait` trait, + * in order to prevent infinite loops. + * + * @return ReflectionClass|false + */ + protected function extensionGetParentClass(object $instance = null) + { + // Shortcut to prevent infinite loops if the class extends Extendable. + if ($this instanceof Extendable) { + return false; + } + + // Find if any parent uses the Extendable trait + if (!is_null($instance)) { + $reflector = $instance; + } else { + $reflector = new ReflectionClass($this); + } + $parent = $reflector->getParentClass(); + + // If there's no parent, stop here. + if ($parent === false) { + return false; + } + + while (!in_array(ExtendableTrait::class, $parent->getTraitNames())) { + $parent = $parent->getParentClass(); + if ($parent === false) { + break; + } + } + + // If no parent uses the Extendable trait, then return the parent class + if ($parent === false) { + return $reflector->getParentClass(); + } + + // Otherwise, we need to loop through until we find the parent class that doesn't use the Extendable trait + return $this->extensionGetParentClass($parent); + } + + /** + * Determines if the given class reflection contains the given method. + */ + protected function extensionMethodExists(ReflectionClass $class, string $methodName): bool + { + try { + $method = $class->getMethod($methodName); + + if (!$method->isPublic()) { + return false; + } + } catch (ReflectionException $e) { + return false; + } + + return true; + } + + /** + * Calls a method through reflection. + */ + protected function extensionCallMethod(ReflectionClass $class, string $method, array $params) + { + $method = $class->getMethod($method); + return $method->invokeArgs($this, $params); + } } diff --git a/tests/Database/UpdaterTest.php b/tests/Database/UpdaterTest.php index c8e0946e9..ef60d4573 100644 --- a/tests/Database/UpdaterTest.php +++ b/tests/Database/UpdaterTest.php @@ -4,6 +4,8 @@ class UpdaterTest extends TestCase { + protected Updater $updater; + public function setUp(): void { include_once __DIR__.'/../fixtures/database/SampleClass.php'; diff --git a/tests/Foundation/ApplicationTest.php b/tests/Foundation/ApplicationTest.php index 5897fde8a..f138de05a 100644 --- a/tests/Foundation/ApplicationTest.php +++ b/tests/Foundation/ApplicationTest.php @@ -5,6 +5,8 @@ class ApplicationTest extends TestCase { + protected string $basePath; + protected function setUp(): void { // Mock application diff --git a/tests/Html/BlockBuilderTest.php b/tests/Html/BlockBuilderTest.php index bcc0b28d4..a2e1fc6ab 100644 --- a/tests/Html/BlockBuilderTest.php +++ b/tests/Html/BlockBuilderTest.php @@ -4,6 +4,8 @@ class BlockBuilderTest extends TestCase { + protected BlockBuilder $Block; + public function setUp(): void { $this->Block = new BlockBuilder(); diff --git a/tests/Support/EventFakeTest.php b/tests/Support/EventFakeTest.php index f6654e235..5e74b6805 100644 --- a/tests/Support/EventFakeTest.php +++ b/tests/Support/EventFakeTest.php @@ -5,6 +5,8 @@ class EventFakeTest extends TestCase { + protected EventFake $faker; + public function setUp(): void { $this->faker = new EventFake(new Dispatcher);