Skip to content

Commit

Permalink
Adding ForwardsCalls trait
Browse files Browse the repository at this point in the history
  • Loading branch information
lukewatts committed Mar 8, 2024
1 parent dec94a7 commit a479966
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 0 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
68 changes: 68 additions & 0 deletions src/Support/Traits/ForwardsCalls.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php declare(strict_types=1);

namespace SlimFacades\Support\Traits;

trait ForwardsCalls
{
/**
* Forward a method call to the given object.
*
* @param mixed $object
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
protected function forwardCallTo($object, $method, $parameters)
{
try {
return $object->{$method}(...$parameters);
} catch (\Error|\BadMethodCallException $e) {
$pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';

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
));
}
}
101 changes: 101 additions & 0 deletions tests/Support/Traits/ForwardsCallsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace SlimFacades\Tests\Support;

use SlimFacades\Support\Traits\ForwardsCalls;
use PHPUnit\Framework\TestCase;

class ForwardsCallsTest extends TestCase
{
public function testForwardsCalls()
{
$results = (new ForwardsCallsOne)->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();
}
}

0 comments on commit a479966

Please sign in to comment.