Skip to content

Commit dbc62ca

Browse files
Merge pull request #15 from DaveLiddament/feature/tidy-up
Tidy up
2 parents 93f78a9 + 342865e commit dbc62ca

10 files changed

Lines changed: 125 additions & 11 deletions

File tree

README.md

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
[![PHPStan level 8](https://img.shields.io/badge/PHPStan-max%20level-brightgreen.svg)](https://github.com/DaveLiddament/php-language-extensions/blob/main/phpstan.neon)
1111

1212

13-
14-
This library provides attributes for extending the PHP language (e.g. adding `package` visibility).
13+
This library provides [attributes](https://www.php.net/manual/en/language.attributes.overview.php) that are used by static analysers to enforce new language features.
1514
The intention, at least initially, is that these extra language features are enforced by static analysis tools (such as Psalm, PHPStan and, ideally, PhpStorm) and NOT at runtime.
1615

1716
**Language feature added:**
@@ -51,7 +50,7 @@ Use one of these:
5150

5251
### PHPStan
5352

54-
To use PHPStan to enforce package level visibility add [this extension](https://github.com/DaveLiddament/phpstan-php-language-extensions).
53+
If you're using PHPStan then use [this extension](https://github.com/DaveLiddament/phpstan-php-language-extensions) to enforce the rules.
5554

5655
```shell
5756
composer require --dev dave-liddament/phpstan-php-language-extensions
@@ -330,7 +329,7 @@ class PersonValidator {
330329
}
331330
```
332331

333-
By default, only constructor arguments are checked. Most DI should be done via constructor injection.
332+
By default, only constructor arguments are checked. Most DI should be done via constructor injection.
334333

335334
In cases where dependencies are injected by methods that aren't constructors, the method must be marked with a `#[CheckInjectableVersion]`:
336335

@@ -353,6 +352,70 @@ class MyService
353352

354353

355354

355+
356+
## Sealed
357+
358+
This is inspired by the rejected [sealed classes RFC](https://wiki.php.net/rfc/sealed_classes)
359+
360+
The `#[Sealed]` attribute takes a list of classes or interfaces that can extend/implement the class/interface.
361+
362+
E.g.
363+
364+
```php
365+
366+
#[Sealed([Success::class, Failure::class])]
367+
abstract class Result {} // Result can only be extended by Success or Failure
368+
369+
// OK
370+
class Success extends Result {}
371+
372+
// OK
373+
class Failure extends Result {}
374+
375+
// ERROR AnotherClass is not allowed to extend Result
376+
class AnotherClass extends Result {}
377+
```
378+
379+
380+
## TestTag
381+
382+
The `#[TestTag]` attribute is an idea borrowed from hardware testing. Methods marked with this attribute are only available to test code.
383+
384+
E.g.
385+
386+
```php
387+
class Person {
388+
389+
#[TestTag]
390+
public function setId(int $id)
391+
{
392+
$this->id = $id;
393+
}
394+
}
395+
396+
397+
function updatePersonId(Person $person): void
398+
{
399+
$person->setId(10); // ERROR - not test code.
400+
}
401+
402+
403+
class PersonTest
404+
{
405+
public function setup(): void
406+
{
407+
$person = new Person();
408+
$person->setId(10); // OK - This is test code.
409+
}
410+
}
411+
```
412+
413+
NOTES:
414+
- Methods with the`#[TestTag]` MUST have public visibility.
415+
- For determining what is "test code" see the relevant plugin. E.g. the [PHPStan extension](https://github.com/DaveLiddament/phpstan-php-language-extensions) can be setup to either:
416+
- Assume all classes that end `Test` is test code. See [className config option](https://github.com/DaveLiddament/phpstan-php-language-extensions#exclude-checks-on-class-names-ending-with-test).
417+
- Assume all classes within a given namespace is test code. See [namespace config option](https://github.com/DaveLiddament/phpstan-php-language-extensions#exclude-checks-based-on-test-namespace).
418+
356419
## Further examples
357420

358421
More detailed examples of how to use attributes is found in [examples](examples/).

examples/sealed/sealedClasses.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SealedClasses;
6+
7+
use DaveLiddament\PhpLanguageExtensions\Sealed;
8+
9+
class Success extends Response // OK
10+
{
11+
}
12+
13+
class Failed extends Response // OK
14+
{
15+
}
16+
17+
#[Sealed(Success::class, Failed::class)]
18+
class Response
19+
{
20+
}
21+
22+
23+
class AnotherClass extends Response // ERROR AnotherClass can not extend Response
24+
{
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SealedInterfaces;
6+
7+
use DaveLiddament\PhpLanguageExtensions\Sealed;
8+
9+
class Success implements Response // OK
10+
{
11+
}
12+
13+
class Failed implements Response // OK
14+
{
15+
}
16+
17+
#[Sealed(Success::class, Failed::class)]
18+
interface Response
19+
{
20+
}
21+
22+
23+
class AnotherClass implements Response // ERROR AnotherClass can not implement Response
24+
{
25+
}

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ parameters:
44
- src
55

66
ignoreErrors:
7-
- '#^Constructor of class DaveLiddament\\PhpLanguageExtensions\\[a-zA-Z]+ has an unused parameter \$[a-z]+\.$#'
7+
- '#^Constructor of class DaveLiddament\\PhpLanguageExtensions\\[a-zA-Z]+ has an unused parameter \$[a-zA-Z]+\.$#'

src/CheckInjectableVersion.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
namespace DaveLiddament\PhpLanguageExtensions;
66

77
#[\Attribute(\Attribute::TARGET_METHOD)]
8-
class CheckInjectableVersion
8+
final class CheckInjectableVersion
99
{
1010
}

src/Friend.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* Limits calling methods to those listed as the method's or class's friends.
99
*/
1010
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
11-
class Friend
11+
final class Friend
1212
{
1313
/** @param class-string ...$friends */
1414
public function __construct(

src/InjectableVersion.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
namespace DaveLiddament\PhpLanguageExtensions;
66

77
#[\Attribute(\Attribute::TARGET_CLASS)]
8-
class InjectableVersion
8+
final class InjectableVersion
99
{
1010
}

src/Package.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
* Limit calls to classes or methods with the Package attribute to calls from classes in the name namespace.
99
*/
1010
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
11-
class Package
11+
final class Package
1212
{
1313
}

src/Sealed.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* Limits the classes that can extend/implement to those listed in $permitted.
99
*/
1010
#[\Attribute(\Attribute::TARGET_CLASS)]
11-
class Sealed
11+
final class Sealed
1212
{
1313
/** @param class-string ...$permitted */
1414
public function __construct(

src/TestTag.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
/**
88
* Add the TestTag attribute to a method that should only be called by test code.
9+
* Attempts to call from non-test code will raise an issue.
910
*/
1011
#[\Attribute(\Attribute::TARGET_METHOD)]
11-
class TestTag
12+
final class TestTag
1213
{
1314
}

0 commit comments

Comments
 (0)