From a4799667b99e4315a6c0d7e92a3c4424cfaa6d70 Mon Sep 17 00:00:00 2001 From: Luke Watts Date: Fri, 8 Mar 2024 16:16:17 +0000 Subject: [PATCH] Adding ForwardsCalls trait --- README.md | 52 +++++++++++ src/Support/Traits/ForwardsCalls.php | 68 ++++++++++++++ tests/Support/Traits/ForwardsCallsTest.php | 101 +++++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 src/Support/Traits/ForwardsCalls.php create mode 100644 tests/Support/Traits/ForwardsCallsTest.php diff --git a/README.md b/README.md index 08d1bba..74e2694 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,58 @@ DumpableCollection {#69 ▼ __NOTE: You can also pass `...$args` to the dd and dump methods as normal if you want to append additional dump data.__ +### ForwardsCalls + +Proxy calls to missing methods in current class, to another target class. Useful when you cannot inherit or modify a class but you want to add some functionality to it (other than overloading any of it's methods of course). + +Here's an example where we have a base `App` class, but it is a final class so we cannot inherit it. So instead, we create an `AppProxy` class which allows us to say that "any method that gets called on `AppProxy` which doesn't exist in `AppProxy`, we use `App` instead" + +```php +class AppProxy +{ + use ForwardsCalls; + + public function __call($method, $parameters) + { + return $this->forwardCallTo(new App, $method, $parameters); + } + + public function addSomeServiceDirectlyToContainer() + { + $this->getContainer()->set('some-service', function ($container) { + return new SomeService($container->get('some-dependency-already-in-container')); + }); + } +} + +final class App +{ + public function __construct( + protected ContainerInterface $container + ) {} + + public function getContainer() + { + return $this->container; + } +} +``` + +Then we can use `getContainer` (or any other public methods/properties) from `App` by calling out `AppProxy` + +```php +$appProxy = new AppProxy; +$app->addSomeServiceDirectlyToContainer(); +$container = $appProxy->getContainer(); +dd($congainer->get('some-service')); +/* +SomeService {# 46 + # some_service_already_in_container: someServiceAlreadyInContainer {# 30 } + ... +} +*/ +``` + ## Pipeline Support class Pipelines allow for a middleware-like interface to chain processing of tasks. diff --git a/src/Support/Traits/ForwardsCalls.php b/src/Support/Traits/ForwardsCalls.php new file mode 100644 index 0000000..2982f12 --- /dev/null +++ b/src/Support/Traits/ForwardsCalls.php @@ -0,0 +1,68 @@ +{$method}(...$parameters); + } catch (\Error|\BadMethodCallException $e) { + $pattern = '~^Call to undefined method (?P[^:]+)::(?P[^\(]+)\(\)$~'; + + if (! preg_match($pattern, $e->getMessage(), $matches)) { + throw $e; + } + + if ($matches['class'] != get_class($object) || + $matches['method'] != $method) { + throw $e; + } + + static::throwBadMethodCallException($method); + } + } + + /** + * Forward a method call to the given object, returning $this if the forwarded call returned itself. + * + * @param mixed $object + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws \BadMethodCallException + */ + protected function forwardDecoratedCallTo($object, $method, $parameters) + { + $result = $this->forwardCallTo($object, $method, $parameters); + + return $result === $object ? $this : $result; + } + + /** + * Throw a bad method call exception for the given method. + * + * @param string $method + * @return void + * + * @throws \BadMethodCallException + */ + protected static function throwBadMethodCallException($method) + { + throw new \BadMethodCallException(sprintf( + 'Call to undefined method %s::%s()', static::class, $method + )); + } +} diff --git a/tests/Support/Traits/ForwardsCallsTest.php b/tests/Support/Traits/ForwardsCallsTest.php new file mode 100644 index 0000000..2eb79ac --- /dev/null +++ b/tests/Support/Traits/ForwardsCallsTest.php @@ -0,0 +1,101 @@ +forwardedTwo('foo', 'bar'); + + $this->assertEquals(['foo', 'bar'], $results); + } + + public function testNestedForwardCalls() + { + $results = (new ForwardsCallsOne)->forwardedBase('foo', 'bar'); + + $this->assertEquals(['foo', 'bar'], $results); + } + + public function testMissingForwardedCallThrowsCorrectError() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Call to undefined method SlimFacades\Tests\Support\ForwardsCallsOne::missingMethod()'); + + (new ForwardsCallsOne)->missingMethod('foo', 'bar'); + } + + public function testMissingAlphanumericForwardedCallThrowsCorrectError() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Call to undefined method SlimFacades\Tests\Support\ForwardsCallsOne::this1_shouldWork_too()'); + + (new ForwardsCallsOne)->this1_shouldWork_too('foo', 'bar'); + } + + public function testNonForwardedErrorIsNotTamperedWith() + { + $this->expectException(\Error::class); + $this->expectExceptionMessage('Call to undefined method SlimFacades\Tests\Support\ForwardsCallsBase::missingMethod()'); + + (new ForwardsCallsOne)->baseError('foo', 'bar'); + } + + public function testThrowBadMethodCallException() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Call to undefined method SlimFacades\Tests\Support\ForwardsCallsOne::test()'); + + (new ForwardsCallsOne)->throwTestException('test'); + } +} + +class ForwardsCallsOne +{ + use ForwardsCalls; + + public function __call($method, $parameters) + { + return $this->forwardCallTo(new ForwardsCallsTwo, $method, $parameters); + } + + public function throwTestException($method) + { + static::throwBadMethodCallException($method); + } +} + +class ForwardsCallsTwo +{ + use ForwardsCalls; + + public function __call($method, $parameters) + { + return $this->forwardCallTo(new ForwardsCallsBase, $method, $parameters); + } + + public function forwardedTwo(...$parameters) + { + return $parameters; + } +} + +/** + * @method void missingMethod() // just stop the red squiggly + */ +class ForwardsCallsBase +{ + public function forwardedBase(...$parameters) + { + return $parameters; + } + + public function baseError() + { + return $this->missingMethod(); + } +}