Skip to content

Commit 530b8c6

Browse files
authored
Merge pull request #39 from jdecool/feat-attribute-2.5
Add attribute management
2 parents 135be8e + 518eac4 commit 530b8c6

File tree

9 files changed

+412
-3
lines changed

9 files changed

+412
-3
lines changed

README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Supported annotations:
1010
* `@server` for `$_SERVER`
1111
* `@putenv` for [`putenv()`](http://php.net/putenv)
1212

13+
Supported attributes:
14+
15+
* `#[Env]` for `$_ENV`
16+
* `#[Server]` for `$_SERVER`
17+
* `#[Putenv]` for [`putenv()`](http://php.net/putenv)
18+
1319
Global variables are set before each test case is executed,
1420
and brought to the original state after each test case has finished.
1521
The same applies to `putenv()`/`getenv()` calls.
@@ -52,13 +58,14 @@ Enable the globals annotation extension in your PHPUnit configuration:
5258
<!-- ... -->
5359

5460
<extensions>
55-
<extension class="Zalas\PHPUnit\Globals\AnnotationExtension" />
61+
<extension class="Zalas\PHPUnit\Globals\AnnotationExtension" /> <!-- if you want to use annotations -->
62+
<extension class="Zalas\PHPUnit\Globals\AttributeExtension" /> <!-- if you want to use attributes -->
5663
</extensions>
5764

5865
</phpunit>
5966
```
6067

61-
Make sure the `AnnotationExtension` is registered before any other extensions that might depend on global variables.
68+
Make sure the `AnnotationExtension` or `AttributeExtension` is registered before any other extensions that might depend on global variables.
6269

6370
Global variables can now be defined in annotations:
6471

@@ -89,6 +96,34 @@ class ExampleTest extends TestCase
8996
}
9097
```
9198

99+
Global variables can also be defined with attributes:
100+
101+
```php
102+
use PHPUnit\Framework\TestCase;
103+
use Zalas\PHPUnit\Globals\Attribute\Env;
104+
use Zalas\PHPUnit\Globals\Attribute\Server;
105+
use Zalas\PHPUnit\Globals\Attribute\Putenv;
106+
107+
#[Env('FOO=bar')]
108+
class ExampleTest extends TestCase
109+
{
110+
#[Env('APP_ENV=foo')]
111+
#[Env('APP_DEBUG=0')]
112+
#[Server('APP_ENV=bar')]
113+
#[Server('APP_DEBUG=1')]
114+
#[Putenv('APP_HOST=localhost')]
115+
public function test_global_variables()
116+
{
117+
$this->assertSame('bar', $_ENV['FOO']);
118+
$this->assertSame('foo', $_ENV['APP_ENV']);
119+
$this->assertSame('0', $_ENV['APP_DEBUG']);
120+
$this->assertSame('bar', $_SERVER['APP_ENV']);
121+
$this->assertSame('1', $_SERVER['APP_DEBUG']);
122+
$this->assertSame('localhost', \getenv('APP_HOST'));
123+
}
124+
}
125+
```
126+
92127
It's also possible to mark a variable as _unset_ so it will not be present in any of the global variables:
93128

94129
```php

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
</php>
1515
<extensions>
1616
<extension class="Zalas\PHPUnit\Globals\AnnotationExtension"/>
17+
<extension class="Zalas\PHPUnit\Globals\AttributeExtension"/>
1718
</extensions>
1819
</phpunit>

src/Attribute/Env.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
use Attribute;
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
9+
final class Env
10+
{
11+
use TargetAware;
12+
13+
public function __construct(
14+
public string $name,
15+
public string $value = '',
16+
public bool $unset = false,
17+
) {
18+
}
19+
}

src/Attribute/Putenv.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
use Attribute;
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
9+
final class Putenv
10+
{
11+
use TargetAware;
12+
13+
public function __construct(
14+
public string $name,
15+
public string $value = '',
16+
public bool $unset = false,
17+
) {
18+
}
19+
}

src/Attribute/Server.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
use Attribute;
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
9+
final class Server
10+
{
11+
use TargetAware;
12+
13+
public function __construct(
14+
public string $name,
15+
public string $value = '',
16+
public bool $unset = false,
17+
) {
18+
}
19+
}

src/Attribute/TargetAware.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
trait TargetAware
7+
{
8+
private int $target = 0;
9+
10+
/**
11+
* @internal
12+
*/
13+
public function withTarget(int $target): self
14+
{
15+
$clone = clone $this;
16+
$clone->target = $target;
17+
18+
return $clone;
19+
}
20+
21+
/**
22+
* @internal
23+
*/
24+
public function getTarget(): int
25+
{
26+
return $this->target;
27+
}
28+
}

src/AttributeExtension.php

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals;
5+
6+
use PHPUnit\Runner\AfterTestHook;
7+
use PHPUnit\Runner\BeforeTestHook;
8+
use Zalas\PHPUnit\Globals\Attribute\Env;
9+
use Zalas\PHPUnit\Globals\Attribute\Putenv;
10+
use Zalas\PHPUnit\Globals\Attribute\Server;
11+
12+
final class AttributeExtension implements BeforeTestHook, AfterTestHook
13+
{
14+
private $server;
15+
private $env;
16+
private $getenv;
17+
18+
public function executeBeforeTest(string $test): void
19+
{
20+
$this->backupGlobals();
21+
$this->readGlobalAttributes($test);
22+
}
23+
24+
public function executeAfterTest(string $test, float $time): void
25+
{
26+
$this->restoreGlobals();
27+
}
28+
29+
private function backupGlobals(): void
30+
{
31+
$this->server = $_SERVER;
32+
$this->env = $_ENV;
33+
$this->getenv = \getenv();
34+
}
35+
36+
private function restoreGlobals(): void
37+
{
38+
$_SERVER = $this->server;
39+
$_ENV = $this->env;
40+
41+
foreach (\array_diff_assoc($this->getenv, \getenv()) as $name => $value) {
42+
\putenv(\sprintf('%s=%s', $name, $value));
43+
}
44+
foreach (\array_diff_assoc(\getenv(), $this->getenv) as $name => $value) {
45+
\putenv($name);
46+
}
47+
}
48+
49+
private function readGlobalAttributes(string $test)
50+
{
51+
$globalVars = $this->parseGlobalAttributes($test);
52+
53+
foreach ($globalVars['env'] as $name => $value) {
54+
$_ENV[$name] = $value;
55+
}
56+
foreach ($globalVars['server'] as $name => $value) {
57+
$_SERVER[$name] = $value;
58+
}
59+
foreach ($globalVars['putenv'] as $name => $value) {
60+
\putenv(\sprintf('%s=%s', $name, $value));
61+
}
62+
63+
$unsetVars = $this->findUnsetVarAttributes($test);
64+
65+
foreach ($unsetVars['unset-env'] as $name) {
66+
unset($_ENV[$name]);
67+
}
68+
foreach ($unsetVars['unset-server'] as $name) {
69+
unset($_SERVER[$name]);
70+
}
71+
foreach ($unsetVars['unset-getenv'] as $name) {
72+
\putenv($name);
73+
}
74+
}
75+
76+
private function parseGlobalAttributes(string $test): array
77+
{
78+
$globals = ['env' => [], 'server' => [], 'putenv' => []];
79+
80+
$attributes = $this->findSetVarAttributes($test);
81+
foreach ($attributes as $attribute) {
82+
match (true) {
83+
$attribute instanceof Env => $globals['env'][$attribute->name] = $attribute->value,
84+
$attribute instanceof Server => $globals['server'][$attribute->name] = $attribute->value,
85+
$attribute instanceof Putenv => $globals['putenv'][$attribute->name] = $attribute->value,
86+
};
87+
}
88+
89+
return $globals;
90+
}
91+
92+
private function findSetVarAttributes(string $test): array
93+
{
94+
$attributes = $this->parseTestMethodAttributes($test);
95+
96+
$attributes = \array_filter(
97+
$attributes,
98+
static fn (Env|Server|Putenv $attribute) => false === $attribute->unset,
99+
ARRAY_FILTER_USE_BOTH
100+
);
101+
102+
\usort($attributes, static fn (Env|Server|Putenv $a, Env|Server|Putenv $b) => $a->getTarget() <=> $b->getTarget());
103+
104+
return $attributes;
105+
}
106+
107+
private function findUnsetVarAttributes(string $test): array
108+
{
109+
$unset = ['unset-env' => [], 'unset-server' => [], 'unset-getenv' => []];
110+
111+
$attributes = $this->parseTestMethodAttributes($test);
112+
foreach ($attributes as $attribute) {
113+
if (false === $attribute->unset) {
114+
continue;
115+
}
116+
117+
match (true) {
118+
$attribute instanceof Env => $unset['unset-env'][] = $attribute->name,
119+
$attribute instanceof Server => $unset['unset-server'][] = $attribute->name,
120+
$attribute instanceof Putenv => $unset['unset-getenv'][] = $attribute->name,
121+
};
122+
}
123+
124+
return $unset;
125+
}
126+
127+
private function parseTestMethodAttributes(string $test): array
128+
{
129+
$parts = \preg_split('/ |::/', $test);
130+
131+
if (!\class_exists($parts[0])) {
132+
return [];
133+
}
134+
135+
$methodAttributes = [];
136+
if (!empty($parts[1])) {
137+
$methodAttributes = $this->collectGlobalsFromAttributes(
138+
(new \ReflectionMethod($parts[0], $parts[1]))->getAttributes()
139+
);
140+
}
141+
142+
return \array_merge(
143+
$this->collectGlobalsFromAttributes((new \ReflectionClass($parts[0]))->getAttributes()),
144+
$methodAttributes,
145+
);
146+
}
147+
148+
private function collectGlobalsFromAttributes(array $attributes): array
149+
{
150+
$globals = [];
151+
152+
foreach ($attributes as $attribute) {
153+
if (!\str_starts_with($attribute->getName(), 'Zalas\\PHPUnit\\Globals\\Attribute\\')) {
154+
continue;
155+
}
156+
157+
/** @var Env|Server|Putenv $instance */
158+
$instance = $attribute->newInstance();
159+
$globals[] = $instance->withTarget($attribute->getTarget());
160+
}
161+
162+
return $globals;
163+
}
164+
}

tests/AnnotationExtensionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public function test_it_backups_the_state()
110110
}
111111

112112
/**
113-
* @depends test_it_backups_the_state
113+
* @depends self::test_it_backups_the_state
114114
*/
115115
public function test_it_cleans_up_after_itself()
116116
{

0 commit comments

Comments
 (0)