-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This trait exposes three methods: 1. `defineMethod(string $class, string $name, \Closure $closure, string $visibility = 'public', bool $static = false): self` 2. `redefineMethod(string $class, string $name, ?\Closure $closure, ?string $visibility = null, ?bool $static = null): self` 3. `deleteMethod(string $class, string $name): self` Refs #12.
- Loading branch information
1 parent
fd0476c
commit 3d3b7a4
Showing
9 changed files
with
840 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Managing Methods | ||
|
||
When writing tests, we often make use of [test doubles](https://en.wikipedia.org/wiki/Test_double) to better control how our code will behave. For instance, we don't want to make calls to remote APIs every time we run our tests, as these dependencies can make our tests fragile and slow. | ||
|
||
If your software is written using proper [Dependency Injection](https://phptherightway.com/#dependency_injection), it's usually pretty easy to [create test doubles with PHPUnit](https://jmauerhan.wordpress.com/2018/10/04/the-5-types-of-test-doubles-and-how-to-create-them-in-phpunit/) and inject them into the objects we create in our tests. | ||
|
||
What happens when the software we're working with isn't coded so nicely, though? | ||
|
||
Most of the time, we can get around this using [Reflection](https://www.php.net/intro.reflection), but _sometimes_ we need a sledgehammer to break through. That's where the `AssertWell\PHPUnitGlobalState\Methods` trait (powered by [PHP's runkit7 extension](Runkit.md)) comes in handy. | ||
|
||
|
||
## Methods | ||
|
||
As all of these methods require [runkit7](Runkit.md), tests that use these methods will automatically be marked as skipped if the extension is unavailable. | ||
|
||
--- | ||
|
||
### defineMethod() | ||
|
||
Define a new method for the duration of the test. | ||
|
||
`defineMethod(string $class, string $name, \Closure $closure, string $visibility = 'public', bool $static = false): self` | ||
|
||
This is a wrapper around [PHP's `runkit7_method_define()` function](https://www.php.net/manual/en/function.runkit7-method-define.php). | ||
|
||
#### Parameters | ||
|
||
<dl> | ||
<dt>$class</dt> | ||
<dd>The class name.</dd> | ||
<dt>$name</dt> | ||
<dd>The method name.</dd> | ||
<dt>$closure</dt> | ||
<dd>The code for the method.</dd> | ||
<dt>$visibility</dt> | ||
<dd>Optional. The method visibility, one of "public", "protected", or "private".</dd> | ||
<dt>$static</dt> | ||
<dd>Optional. Whether or not the method should be static. Default is false.</dd> | ||
</dl> | ||
|
||
#### Return values | ||
|
||
This method will return the calling class, enabling multiple methods to be chained. | ||
|
||
An `AssertWell\PHPUnitGlobalState\Exceptions\MethodExistsException` exception will be thrown if the given `$method` already exists. An `AssertWell\PHPUnitGlobalState\Exceptions\RunkitException` will be thrown if the given method cannot be defined. | ||
|
||
--- | ||
|
||
### redefineMethod() | ||
|
||
Redefine an existing method for the duration of the test. If `$name` does not exist, it will be defined. | ||
|
||
`defineMethod(string $class, string $name, ?\Closure $closure, string $visibility = 'public', bool $static = false): self` | ||
|
||
This is a wrapper around [PHP's `runkit7_method_redefine()` function](https://www.php.net/manual/en/function.runkit7-method-redefine.php). | ||
|
||
#### Parameters | ||
|
||
<dl> | ||
<dt>$class</dt> | ||
<dd>The class name.</dd> | ||
<dt>$name</dt> | ||
<dd>The method name.</dd> | ||
<dt>$closure</dt> | ||
<dd>The new code for the method.</dd> | ||
<dd>If <code>null</code> is passed, the existing method body will be copied.</dd> | ||
<dt>$visibility</dt> | ||
<dd>Optional. The method visibility, one of "public", "protected", or "private".</dd> | ||
<dt>$static</dt> | ||
<dd>Optional. Whether or not the method should be static. Default is false.</dd> | ||
</dl> | ||
|
||
#### Return values | ||
|
||
This method will return the calling class, enabling multiple methods to be chained. | ||
|
||
An `AssertWell\PHPUnitGlobalState\Exceptions\RunkitException` will be thrown if the given method cannot be (re)defined. | ||
|
||
--- | ||
|
||
### deleteMethod() | ||
|
||
Delete/undefine a method for the duration of the single test. | ||
|
||
`deleteMethod(string $class, string $name): self` | ||
|
||
#### Parameters | ||
|
||
<dl> | ||
<dt>$class</dt> | ||
<dd>The class name.</dd> | ||
<dt>$name</dt> | ||
<dd>The method name.</dd> | ||
</dl> | ||
|
||
#### Return values | ||
|
||
This method will return the calling class, enabling multiple methods to be chained. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?php | ||
|
||
namespace AssertWell\PHPUnitGlobalState\Exceptions; | ||
|
||
class MethodExistsException extends FunctionExistsException | ||
{ | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
<?php | ||
|
||
namespace AssertWell\PHPUnitGlobalState; | ||
|
||
use AssertWell\PHPUnitGlobalState\Exceptions\MethodExistsException; | ||
use AssertWell\PHPUnitGlobalState\Exceptions\RunkitException; | ||
use AssertWell\PHPUnitGlobalState\Support\Runkit; | ||
|
||
trait Methods | ||
{ | ||
/** | ||
* All methods being handled by this trait. | ||
* | ||
* @var array[] | ||
*/ | ||
private $methods = [ | ||
'defined' => [], | ||
'redefined' => [], | ||
]; | ||
|
||
/** | ||
* @after | ||
* | ||
* @return void | ||
*/ | ||
protected function restoreMethods() | ||
{ | ||
// Reset anything that was modified. | ||
array_walk($this->methods['redefined'], function ($methods, $class) { | ||
foreach ($methods as $modified => $original) { | ||
if (method_exists($class, $modified)) { | ||
Runkit::method_remove($class, $modified); | ||
} | ||
|
||
// Put the original back into place. | ||
Runkit::method_rename($class, $original, $modified); | ||
} | ||
|
||
unset($this->methods['redefined'][$class]); | ||
}); | ||
|
||
array_walk($this->methods['defined'], function ($methods, $class) { | ||
foreach ($methods as $method) { | ||
Runkit::method_remove($class, $method); | ||
} | ||
unset($this->methods['defined'][$class]); | ||
}); | ||
|
||
Runkit::reset(); | ||
} | ||
|
||
/** | ||
* Define a new method. | ||
* | ||
* @throws \AssertWell\PHPUnitGlobalState\Exceptions\MethodExistsException | ||
* @throws \AssertWell\PHPUnitGlobalState\Exceptions\RunkitException | ||
* | ||
* @param string $class The class name. | ||
* @param string $name The method name. | ||
* @param \Closure $closure The method body. | ||
* @param string $visibility Optional. The method visibility, one of "public", "protected", | ||
* or "private". Default is "public". | ||
* @param bool $static Optional. Whether or not the method should be defined as static. | ||
* Default is false. | ||
* | ||
* @return self | ||
*/ | ||
protected function defineMethod($class, $name, \Closure $closure, $visibility = 'public', $static = false) | ||
{ | ||
if (method_exists($class, $name)) { | ||
throw new MethodExistsException(sprintf( | ||
'Method %1$s::%2$s() already exists. You may redefine it using %3$s::redefineMethod() instead.', | ||
$class, | ||
$name, | ||
get_class($this) | ||
)); | ||
} | ||
|
||
if (! Runkit::isAvailable()) { | ||
$this->markTestSkipped('defineMethod() requires Runkit be available, skipping.'); | ||
} | ||
|
||
$flags = Runkit::getVisibilityFlags($visibility, $static); | ||
|
||
if (! Runkit::method_add($class, $name, $closure, $flags)) { | ||
throw new RunkitException(sprintf('Unable to define method %1$s::%2$s().', $class, $name)); | ||
} | ||
|
||
if (! isset($this->methods['defined'][$class])) { | ||
$this->methods['defined'][$class] = []; | ||
} | ||
$this->methods['defined'][$class][] = $name; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Redefine an existing method. | ||
* | ||
* If the method doesn't yet exist, it will be defined. | ||
* | ||
* @param string $class The class name. | ||
* @param string $name The method name. | ||
* @param \Closure|null $closure Optional. A closure representing the method body. If null, | ||
* the method body will not be replaced. Default is null. | ||
* @param string $visibility Optional. The method visibility, one of "public", | ||
* "protected", or "private". Default is the same as the | ||
* current value. | ||
* @param bool $static Optional. Whether or not the method should be defined as | ||
* static. Default is the same is as the current value. | ||
* | ||
* @return self | ||
*/ | ||
protected function redefineMethod($class, $name, $closure = null, $visibility = null, $static = null) | ||
{ | ||
if (! method_exists($class, $name)) { | ||
return $this->defineMethod($class, $name, $closure, $visibility, $static); | ||
} | ||
|
||
if (! Runkit::isAvailable()) { | ||
$this->markTestSkipped('redefineMethod() requires Runkit be available, skipping.'); | ||
} | ||
|
||
$method = new \ReflectionMethod($class, $name); | ||
|
||
if (null === $visibility) { | ||
if ($method->isPrivate()) { | ||
$visibility = 'private'; | ||
} elseif ($method->isProtected()) { | ||
$visibility = 'protected'; | ||
} else { | ||
$visibility = 'public'; | ||
} | ||
} | ||
|
||
if (null === $static) { | ||
$static = $method->isStatic(); | ||
} | ||
|
||
$flags = Runkit::getVisibilityFlags($visibility, $static); | ||
|
||
// If $closure is null, copy the existing method body. | ||
if (null === $closure) { | ||
$closure = $method->isStatic() | ||
? $method->getClosure() | ||
: $method->getClosure($this->getMockBuilder($class) | ||
->disableOriginalConstructor() | ||
->getMock()); | ||
} | ||
|
||
// Back up the original version of the method. | ||
if (! isset($this->methods['redefined'][$class][$name])) { | ||
$prefixed = Runkit::makePrefixed($name); | ||
|
||
if (! Runkit::method_rename($class, $name, $prefixed)) { | ||
throw new RunkitException( | ||
sprintf('Unable to back up %1$s::%2$s(), aborting.', $class, $name) | ||
); | ||
} | ||
|
||
if (! isset($this->methods['redefined'][$class])) { | ||
$this->methods['redefined'][$class] = []; | ||
} | ||
$this->methods['redefined'][$class][$name] = $prefixed; | ||
|
||
if (! Runkit::method_add($class, $name, $closure, $flags)) { | ||
throw new RunkitException( | ||
sprintf('Unable to redefine function %1$s::%2$s().', $method, $name) | ||
); | ||
} | ||
} else { | ||
Runkit::method_redefine($class, $name, $closure, $flags); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Delete an existing method. | ||
* | ||
* @param string $class The class name. | ||
* @param string $name The method to be deleted. | ||
* | ||
* @return self | ||
*/ | ||
protected function deleteMethod($class, $name) | ||
{ | ||
if (! method_exists($class, $name)) { | ||
return $this; | ||
} | ||
|
||
$prefixed = Runkit::makePrefixed($name); | ||
|
||
if (! Runkit::method_rename($class, $name, $prefixed)) { | ||
throw new RunkitException( | ||
sprintf('Unable to back up %1$s::%2$s(), aborting.', $class, $name) | ||
); | ||
} | ||
|
||
if (! isset($this->methods['redefined'][$class])) { | ||
$this->methods['redefined'][$class] = []; | ||
} | ||
$this->methods['redefined'][$class][$name] = $prefixed; | ||
|
||
return $this; | ||
} | ||
} |
Oops, something went wrong.