Skip to content

Commit 23b5110

Browse files
Merge pull request #32 from eliashaeussler/feature/forbids-package
[FEATURE] Introduce `#[ForbidsPackage]` attribute
2 parents e67a5fd + d4d1a65 commit 23b5110

28 files changed

+1178
-35
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ The following attributes are shipped with this library:
6666

6767
* [`#[ForbidsClass]`](docs/attributes/forbids-class.md)
6868
* [`#[ForbidsConstant]`](docs/attributes/forbids-constant.md)
69+
* [`#[ForbidsPackage]`](docs/attributes/forbids-package.md)
6970
* [`#[RequiresClass]`](docs/attributes/requires-class.md)
7071
* [`#[RequiresConstant]`](docs/attributes/requires-constant.md)
7172
* [`#[RequiresPackage]`](#requirespackage)

docs/attributes/forbids-package.md

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# [`#[ForbidsPackage]`](../../src/Attribute/ForbidsPackage.php)
2+
3+
_Scope: Class & Method level_
4+
5+
This attribute can be used to define packages which should *not* be
6+
installed via Composer in the current environment. It can be specified
7+
for single tests as well as complete test classes. You can optionally
8+
define a version constraint and a custom message.
9+
10+
> [!IMPORTANT]
11+
> The attribute determines installed Composer packages from the build-time
12+
> generated `InstalledVersions` class built by Composer. In order to properly
13+
> read from this class, it's essential to include Composer's generated
14+
> autoloader in your PHPUnit bootstrap script:
15+
>
16+
> ```xml
17+
> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
18+
> xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
19+
> bootstrap="vendor/autoload.php"
20+
> >
21+
> <!-- ... -->
22+
> </phpunit>
23+
> ```
24+
>
25+
> You can also pass the script as command option: `phpunit --bootstrap vendor/autoload.php`
26+
27+
## Configuration
28+
29+
By default, test cases with satisfied requirements are skipped. However, this
30+
behavior can be configured by using the `handleSatisfiedPackageRequirements`
31+
extension parameter. If set to `fail`, test cases with satisfied requirements
32+
will fail (defaults to `skip`):
33+
34+
```xml
35+
<extensions>
36+
<bootstrap class="EliasHaeussler\PHPUnitAttributes\PHPUnitAttributesExtension">
37+
<parameter name="handleSatisfiedPackageRequirements" value="fail" />
38+
</bootstrap>
39+
</extensions>
40+
```
41+
42+
## Example
43+
44+
```php
45+
final class DummyTest extends TestCase
46+
{
47+
#[ForbidsPackage('symfony/console')]
48+
public function testDummyAction(): void
49+
{
50+
// ...
51+
}
52+
}
53+
```
54+
55+
<details>
56+
<summary>More examples</summary>
57+
58+
### Forbid explicit Composer package
59+
60+
Class level:
61+
62+
```php
63+
#[ForbidsPackage('symfony/console')]
64+
final class DummyTest extends TestCase
65+
{
66+
public function testDummyAction(): void
67+
{
68+
// Skipped if symfony/console is installed.
69+
}
70+
71+
public function testOtherDummyAction(): void
72+
{
73+
// Skipped if symfony/console is installed.
74+
}
75+
}
76+
```
77+
78+
Method level:
79+
80+
```php
81+
final class DummyTest extends TestCase
82+
{
83+
#[ForbidsPackage('symfony/console')]
84+
public function testDummyAction(): void
85+
{
86+
// Skipped if symfony/console is installed.
87+
}
88+
89+
public function testOtherDummyAction(): void
90+
{
91+
// Not skipped.
92+
}
93+
}
94+
```
95+
96+
### Forbid any Composer package matching a given pattern
97+
98+
Class level:
99+
100+
```php
101+
#[ForbidsPackage('symfony/*')]
102+
final class DummyTest extends TestCase
103+
{
104+
public function testDummyAction(): void
105+
{
106+
// Skipped if any symfony/* packages are installed.
107+
}
108+
109+
public function testOtherDummyAction(): void
110+
{
111+
// Skipped if any symfony/* packages are installed.
112+
}
113+
}
114+
```
115+
116+
Method level:
117+
118+
```php
119+
final class DummyTest extends TestCase
120+
{
121+
#[ForbidsPackage('symfony/*')]
122+
public function testDummyAction(): void
123+
{
124+
// Skipped if any symfony/* packages are installed.
125+
}
126+
127+
public function testOtherDummyAction(): void
128+
{
129+
// Not skipped.
130+
}
131+
}
132+
```
133+
134+
### Forbid Composer package with given version constraint
135+
136+
Class level:
137+
138+
```php
139+
#[ForbidsPackage('symfony/console', '>= 7')]
140+
final class DummyTest extends TestCase
141+
{
142+
public function testDummyAction(): void
143+
{
144+
// Skipped if installed version of symfony/console is >= 7.
145+
}
146+
147+
public function testOtherDummyAction(): void
148+
{
149+
// Skipped if installed version of symfony/console is >= 7.
150+
}
151+
}
152+
```
153+
154+
Method level:
155+
156+
```php
157+
final class DummyTest extends TestCase
158+
{
159+
#[ForbidsPackage('symfony/console', '>= 7')]
160+
public function testDummyAction(): void
161+
{
162+
// Skipped if installed version of symfony/console is >= 7.
163+
}
164+
165+
public function testOtherDummyAction(): void
166+
{
167+
// Not skipped.
168+
}
169+
}
170+
```
171+
172+
### Forbid Composer package and provide custom message
173+
174+
Class level:
175+
176+
```php
177+
#[ForbidsPackage('symfony/console', message: 'This test forbids the Symfony Console.')]
178+
final class DummyTest extends TestCase
179+
{
180+
public function testDummyAction(): void
181+
{
182+
// Skipped if symfony/console is installed, along with custom message.
183+
}
184+
185+
public function testOtherDummyAction(): void
186+
{
187+
// Skipped if symfony/console is installed, along with custom message.
188+
}
189+
}
190+
```
191+
192+
Method level:
193+
194+
```php
195+
final class DummyTest extends TestCase
196+
{
197+
#[ForbidsPackage('symfony/console', message: 'This test forbids the Symfony Console.')]
198+
public function testDummyAction(): void
199+
{
200+
// Skipped if symfony/console is installed, along with custom message.
201+
}
202+
203+
public function testOtherDummyAction(): void
204+
{
205+
// Not skipped.
206+
}
207+
}
208+
```
209+
210+
### Forbid Composer package and define custom outcome behavior
211+
212+
Class level:
213+
214+
```php
215+
#[ForbidsPackage('symfony/console', outcomeBehavior: OutcomeBehavior::Fail)]
216+
final class DummyTest extends TestCase
217+
{
218+
public function testDummyAction(): void
219+
{
220+
// Fails if symfony/console is installed.
221+
}
222+
223+
public function testOtherDummyAction(): void
224+
{
225+
// Fails if symfony/console is installed.
226+
}
227+
}
228+
```
229+
230+
Method level:
231+
232+
```php
233+
final class DummyTest extends TestCase
234+
{
235+
#[ForbidsPackage('symfony/console', outcomeBehavior: OutcomeBehavior::Fail)]
236+
public function testDummyAction(): void
237+
{
238+
// Fails if symfony/console is installed.
239+
}
240+
241+
public function testOtherDummyAction(): void
242+
{
243+
// Does not fail.
244+
}
245+
}
246+
```
247+
248+
### Multiple requirements
249+
250+
Class level:
251+
252+
```php
253+
#[ForbidsPackage('symfony/console')]
254+
#[ForbidsPackage('guzzlehttp/guzzle')]
255+
final class DummyTest extends TestCase
256+
{
257+
public function testDummyAction(): void
258+
{
259+
// Skipped if symfony/console and/or guzzlehttp/guzzle are installed.
260+
}
261+
262+
public function testOtherDummyAction(): void
263+
{
264+
// Skipped if symfony/console and/or guzzlehttp/guzzle are installed.
265+
}
266+
}
267+
```
268+
269+
Method level:
270+
271+
```php
272+
final class DummyTest extends TestCase
273+
{
274+
#[ForbidsPackage('symfony/console')]
275+
#[ForbidsPackage('guzzlehttp/guzzle')]
276+
public function testDummyAction(): void
277+
{
278+
// Skipped if symfony/console and/or guzzlehttp/guzzle are installed.
279+
}
280+
281+
public function testOtherDummyAction(): void
282+
{
283+
// Not skipped.
284+
}
285+
}
286+
```
287+
288+
</details>

docs/attributes/requires-package.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ custom message.
1010
> [!IMPORTANT]
1111
> The attribute determines installed Composer packages from the build-time
1212
> generated `InstalledVersions` class built by Composer. In order to properly
13-
> read from this class , it's essential to include Composer's generated
13+
> read from this class, it's essential to include Composer's generated
1414
> autoloader in your PHPUnit bootstrap script:
1515
>
1616
> ```xml

src/Attribute/ForbidsPackage.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Composer package "eliashaeussler/phpunit-attributes".
7+
*
8+
* Copyright (C) 2024-2025 Elias Häußler <elias@haeussler.dev>
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU General Public License as published by
12+
* the Free Software Foundation, either version 3 of the License, or
13+
* (at your option) any later version.
14+
*
15+
* This program is distributed in the hope that it will be useful,
16+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
* GNU General Public License for more details.
19+
*
20+
* You should have received a copy of the GNU General Public License
21+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
24+
namespace EliasHaeussler\PHPUnitAttributes\Attribute;
25+
26+
use Attribute;
27+
use EliasHaeussler\PHPUnitAttributes\Enum;
28+
29+
/**
30+
* ForbidsPackage.
31+
*
32+
* @author Elias Häußler <elias@haeussler.dev>
33+
* @license GPL-3.0-or-later
34+
*/
35+
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
36+
final class ForbidsPackage
37+
{
38+
/**
39+
* @param non-empty-string $package
40+
* @param non-empty-string|null $versionRequirement
41+
* @param non-empty-string|null $message
42+
*/
43+
public function __construct(
44+
private readonly string $package,
45+
private readonly ?string $versionRequirement = null,
46+
private readonly ?string $message = null,
47+
private readonly ?Enum\OutcomeBehavior $outcomeBehavior = null,
48+
) {}
49+
50+
/**
51+
* @return non-empty-string
52+
*/
53+
public function package(): string
54+
{
55+
return $this->package;
56+
}
57+
58+
/**
59+
* @return non-empty-string|null
60+
*/
61+
public function versionRequirement(): ?string
62+
{
63+
return $this->versionRequirement;
64+
}
65+
66+
/**
67+
* @return non-empty-string|null
68+
*/
69+
public function message(): ?string
70+
{
71+
return $this->message;
72+
}
73+
74+
public function outcomeBehavior(): ?Enum\OutcomeBehavior
75+
{
76+
return $this->outcomeBehavior;
77+
}
78+
}

0 commit comments

Comments
 (0)