Skip to content

Commit a357c80

Browse files
authored
Merge pull request #32 from icanhazstring/feature/attribute-reader
Add attribute reader
2 parents 4221666 + 3e6f3d9 commit a357c80

16 files changed

+459
-57
lines changed

README.md

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
# PHPUnit Globals
22

3-
Allows to use annotations to define global variables in PHPUnit test cases.
3+
Allows to use attributes to define global variables in PHPUnit test cases.
44

55
[![Build](https://github.com/jakzal/phpunit-globals/actions/workflows/build.yml/badge.svg)](https://github.com/jakzal/phpunit-globals/actions/workflows/build.yml)
66

7+
Supported attributes:
8+
* `#[Env]` for `$_ENV`
9+
* `#[Server]` for `$_SERVER`
10+
* `#[Putenv]` for [`putenv()`](http://php.net/putenv)
11+
712
Supported annotations:
813

9-
* `@env` for `$_ENV`
10-
* `@server` for `$_SERVER`
11-
* `@putenv` for [`putenv()`](http://php.net/putenv)
14+
* `@env` and `@unset-env` for `$_ENV`
15+
* `@server` and `@unset-server` for `$_SERVER`
16+
* `@putenv` and `@unset-getenv` for [`putenv()`](http://php.net/putenv)
1217

1318
Global variables are set before each test case is executed,
1419
and brought to the original state after each test case has finished.
@@ -39,7 +44,7 @@ Remember to instruct PHPUnit to load extensions in your `phpunit.xml`:
3944

4045
## Usage
4146

42-
Enable the globals annotation extension in your PHPUnit configuration:
47+
Enable the globals attribute extension in your PHPUnit configuration:
4348

4449
```xml
4550
<?xml version="1.0" encoding="UTF-8"?>
@@ -51,31 +56,33 @@ Enable the globals annotation extension in your PHPUnit configuration:
5156
<!-- ... -->
5257

5358
<extensions>
54-
<bootstrap class="Zalas\PHPUnit\Globals\AnnotationExtension" />
59+
<bootstrap class="Zalas\PHPUnit\Globals\AttributeExtension" />
5560
</extensions>
56-
5761
</phpunit>
5862
```
5963

60-
Make sure the `AnnotationExtension` is registered before any other extensions that might depend on global variables.
64+
> If you are using a version before PHP 8.1 you can use the `AnnotationExtension` instead.
65+
66+
Make sure the `AttributeExtension` is registered before any other extensions that might depend on global variables.
6167

62-
Global variables can now be defined in annotations:
68+
Global variables can now be defined in attributes:
6369

6470
```php
6571
use PHPUnit\Framework\TestCase;
72+
use Zalas\PHPUnit\Globals\Attribute\Env;
73+
use Zalas\PHPUnit\Globals\Attribute\Server;
74+
use Zalas\PHPUnit\Globals\Attribute\Putenv;
6675

6776
/**
6877
* @env FOO=bar
6978
*/
7079
class ExampleTest extends TestCase
7180
{
72-
/**
73-
* @env APP_ENV=foo
74-
* @env APP_DEBUG=0
75-
* @server APP_ENV=bar
76-
* @server APP_DEBUG=1
77-
* @putenv APP_HOST=localhost
78-
*/
81+
#[Env('APP_ENV', 'foo')]
82+
#[Env('APP_DEBUG', '0')]
83+
#[Server('APP_ENV', 'bar')]
84+
#[Server('APP_DEBUG', '1')]
85+
#[Putenv('APP_HOST', 'localhost')]
7986
public function test_global_variables()
8087
{
8188
$this->assertSame('bar', $_ENV['FOO']);
@@ -95,11 +102,9 @@ use PHPUnit\Framework\TestCase;
95102

96103
class ExampleTest extends TestCase
97104
{
98-
/**
99-
* @unset-env APP_ENV
100-
* @unset-server APP_DEBUG
101-
* @unset-getenv APP_HOST
102-
*/
105+
#[Env('APP_ENV', unset: true)]
106+
#[Server('APP_DEBUG', unset: true)]
107+
#[Putenv('APP_HOST', unset: true)]
103108
public function test_global_variables()
104109
{
105110
$this->assertArrayNotHasKey('APP_ENV', $_ENV);
@@ -115,15 +120,15 @@ replace the extension registration in `phpunit.xml`:
115120

116121
```xml
117122
<extensions>
118-
<extension class="Zalas\PHPUnit\Globals\AnnotationExtension" />
123+
<extension class="Zalas\PHPUnit\Globals\AttributeExtension" />
119124
</extensions>
120125
```
121126

122127
with:
123128

124129
```xml
125130
<extensions>
126-
<bootstrap class="Zalas\PHPUnit\Globals\AnnotationExtension" />
131+
<bootstrap class="Zalas\PHPUnit\Globals\AttributeExtension" />
127132
</extensions>
128133
```
129134

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zalas/phpunit-globals",
3-
"description": "Allows to use annotations to define global variables in PHPUnit test cases.",
3+
"description": "Allows to use attributes to define global variables in PHPUnit test cases.",
44
"type": "library",
55
"license": "MIT",
66
"authors": [

phpunit.xml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
<env name="USER" value="test" force="true"/>
99
</php>
1010
<extensions>
11-
<bootstrap class="Zalas\PHPUnit\Globals\AnnotationExtension" />
11+
<bootstrap class="Zalas\PHPUnit\Globals\Tests\Stub\CombinedExtension" />
1212
</extensions>
1313
</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 readonly string $name,
15+
public readonly string $value = '',
16+
public readonly 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 readonly string $name,
15+
public readonly string $value = '',
16+
public readonly 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 readonly string $name,
15+
public readonly string $value = '',
16+
public readonly 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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals;
5+
6+
use PHPUnit\Runner\Extension\Extension;
7+
use PHPUnit\Runner\Extension\Facade;
8+
use PHPUnit\Runner\Extension\ParameterCollection;
9+
use PHPUnit\TextUI\Configuration\Configuration;
10+
11+
final class AttributeExtension implements Extension
12+
{
13+
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
14+
{
15+
$globalsBackup = new GlobalsBackup();
16+
$globalsAttributeReader = new GlobalsAttributeReader();
17+
$globalsRestoration = new GlobalsRestoration($globalsBackup);
18+
$facade->registerSubscribers($globalsBackup, $globalsAttributeReader, $globalsRestoration);
19+
}
20+
}

src/GlobalsAttributeReader.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals;
5+
6+
use PHPUnit\Event\Code\TestMethod;
7+
use PHPUnit\Event\Test\PreparationStarted;
8+
use PHPUnit\Event\Test\PreparationStartedSubscriber;
9+
use Zalas\PHPUnit\Globals\Attribute\Env;
10+
use Zalas\PHPUnit\Globals\Attribute\Putenv;
11+
use Zalas\PHPUnit\Globals\Attribute\Server;
12+
13+
final class GlobalsAttributeReader implements PreparationStartedSubscriber
14+
{
15+
public function notify(PreparationStarted $event): void
16+
{
17+
$this->readGlobalAttributes($event->test());
18+
}
19+
20+
private function readGlobalAttributes(TestMethod $method): void
21+
{
22+
$attributes = $this->parseTestMethodAttributes($method);
23+
$setVars = $this->findSetVarAttributes($attributes);
24+
25+
foreach ($setVars as $var) {
26+
match (true) {
27+
$var instanceof Env => $_ENV[$var->name] = $var->value,
28+
$var instanceof Server => $_SERVER[$var->name] = $var->value,
29+
$var instanceof Putenv => \putenv(\sprintf('%s=%s', $var->name, $var->value))
30+
};
31+
}
32+
33+
$unsetVars = $this->findUnsetVarAttributes($attributes);
34+
35+
foreach ($unsetVars as $var) {
36+
$callable = match (true) {
37+
$var instanceof Env => static function ($var) {
38+
unset($_ENV[$var->name]);
39+
},
40+
$var instanceof Server => static function ($var) {
41+
unset($_SERVER[$var->name]);
42+
},
43+
$var instanceof Putenv => static function ($var) {
44+
\putenv($var->name);
45+
}
46+
};
47+
48+
$callable($var);
49+
}
50+
}
51+
52+
/**
53+
* @param array<Env|Putenv|Server> $attributes
54+
*/
55+
private function findSetVarAttributes(array $attributes): array
56+
{
57+
$attributes = \array_filter(
58+
$attributes,
59+
static fn (Env|Server|Putenv $attribute) => false === $attribute->unset,
60+
ARRAY_FILTER_USE_BOTH
61+
);
62+
63+
\usort($attributes, static fn (Env|Server|Putenv $a, Env|Server|Putenv $b) => $a->getTarget() <=> $b->getTarget());
64+
65+
return $attributes;
66+
}
67+
68+
/**
69+
* @param array<Env|Putenv|Server> $attributes
70+
*/
71+
private function findUnsetVarAttributes(array $attributes): array
72+
{
73+
$attributes = \array_filter(
74+
$attributes,
75+
static fn (Env|Server|Putenv $attribute) => true === $attribute->unset,
76+
ARRAY_FILTER_USE_BOTH
77+
);
78+
79+
\usort($attributes, static fn (Env|Server|Putenv $a, Env|Server|Putenv $b) => $a->getTarget() <=> $b->getTarget());
80+
81+
return $attributes;
82+
}
83+
84+
/**
85+
* @return array<Env|Putenv|Server>
86+
*/
87+
private function parseTestMethodAttributes(TestMethod $method): array
88+
{
89+
$className = $method->className();
90+
$methodName = $method->methodName();
91+
92+
$methodAttributes = null;
93+
94+
if (null !== $methodName) {
95+
$methodAttributes = $this->collectGlobalsFromAttributes(
96+
(new \ReflectionMethod($className, $methodName))->getAttributes()
97+
);
98+
}
99+
100+
return \array_merge(
101+
$methodAttributes,
102+
$this->collectGlobalsFromAttributes((new \ReflectionClass($className))->getAttributes())
103+
);
104+
}
105+
106+
/**
107+
* @param array<\ReflectionAttribute> $attributes
108+
* @return array<Env|Putenv|Server>
109+
*/
110+
private function collectGlobalsFromAttributes(array $attributes): array
111+
{
112+
$globals = [];
113+
114+
foreach ($attributes as $attribute) {
115+
if (!\str_starts_with($attribute->getName(), 'Zalas\\PHPUnit\\Globals\\Attribute\\')) {
116+
continue;
117+
}
118+
119+
/** @var Env|Server|Putenv $instance */
120+
$instance = $attribute->newInstance();
121+
$globals[] = $instance->withTarget($attribute->getTarget());
122+
}
123+
124+
return $globals;
125+
}
126+
}

0 commit comments

Comments
 (0)