diff --git a/README.md b/README.md index 57d2be5..93c348f 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ The intention, at least initially, is that these extra language features are enf **Language feature added:** - [Friend](#friend) +- [NamespaceVisibility](#namespaceVisibility) - [InjectableVersion](#injectableVersion) -- [Package](#package) - [Sealed](#sealed) - [TestTag](#testtag) @@ -28,15 +28,17 @@ The intention, at least initially, is that these extra language features are enf - [Psalm](#psalm) - [New Language Features](#new-language-features) - [Friend](#friend) + - [NamespaceVisibility](#namespaceVisibility) - [InjectableVersion](#injectableVersion) - - [Package](#package) - [Sealed](#sealed) - [TestTag](#testtag) + - Deprecated + - [Package](#package) replace with [NamespaceVisibility](#namespaceVisibility) + - [Further examples](#further-examples) - [Contributing](#contributing) - ## Installation To make the attributes available for your codebase use: @@ -65,229 +67,246 @@ Coming soon. ## New language features -## Package +## Friend -The `#[Package]` attribute acts like an extra visibility modifier like `public`, `protected` and `private`. It is inspired by Java's `package` visibility modifier. -The `#[Package]` attribute limits the visibility of a class or method to only being accessible from code in the same namespace. +A method or class can supply via a `#[Friend]` attribute a list of classes. Only these classes can call the method. +This is loosely based on the C++ friend feature. -Example applying `#[Package]` to methods: +In the example below the `Person::__construct` method can only be called from `PersonBuilder`: ```php -namespace Foo { - class Person - { - #[Package] - public function __construct( - private string $name; - ) { +class Person +{ + #[Friend(PersonBuilder::class)] + public function __construct() + { + // Some implementation } - - #[Package] - public function updateName(string $name): void +} + + +class PersonBuilder +{ + public function build(): Person { - $this->name = $name; + $person = new Person(): // OK as PersonBuilder is allowed to call Person's construct method. + // set up Person + return $person; } - - public function getName(): string +} + + +// ERROR Call to Person::__construct is not from PersonBuilder +$person = new Person(); + +``` + +**NOTES:** +- Multiple classes can be specified. E.g. `#[Friend(Foo::class, Bar::class)]` +- A class can have a `#[Friend]` attribute, classes listed here are applied to every method. + ```php + #[Friend(Foo::class)] + class Entity + { + public function ping(): void // ping has friend Bar { - return $this->name; } } - - class PersonFactory + ``` +- The `#[Friend]` attribute is additive. If a class and a method have the `#[Friend]` the method can be called from any of the classes listed. E.g. + ```php + #[Friend(Foo::class)] + class Entity { - public static function create(string $name): Person + #[Friend(Bar::class)] + public function pong(): void // pong has friends Foo and Bar { - return new Person($name); // This is allowed } } -} + ``` +- This is currently limited to method calls (including `__construct`). -namespace Bar { - class Demo + +## NamespaceVisibility + +The `#[NamespaceVisibility]` attribute acts as extra visibility modifier like `public`, `protected` and `private`. +By default, the `#[NamespaceVisibility]` attribute limits the visibility of a class or method to only being accessible from in the same namespace, or sub namespace. + +Example applying `#[NamespaceVisibility]` to the `Telephone::ring` method: + +```php +namespace Foo { + + class Telephone + { + #[NamespaceVisibility] + public function ring(): void + { + } + } + + class Ringer { - public function allowed(): void + public function ring(Telephone $telephone): Person { - // Code below is OK. Only calling public methods - $jane = PersonBuilder::create("Jane"); - echo $jane->getName(); + $telephone->ring(); // OK calling Telephone::ring() from same namespace } + } +} + +namespace Foo\SubNamespace { + + use Foo\Telephone; - public function notAllowed1(Person $person): void + class SubNamespaceRinger + { + public function ring(Telephone $telephone): Person { - // ERROR with line below: `update` method has package visibility. It can only be called from the '`Foo` namespace. - $person->updateName("Robert") + $telephone->ring(); // OK calling Telephone::ring() from sub namespace } + } +} + + +namespace Bar { + + use Foo\Telephone; - public function notAllowed2(): void + class DifferentNamespaceRinger + { + public function ring(Telephone $telephone): Person { - // ERROR with line below. Person's __construct method has package visibility. It can only be called by code in the `Foo` namespace. - $jane = new Person(); + $telephone->ring(); // ERROR calling Telephone::ring() from different namespace } } } ``` -Example applying `#[Package]` to classes: +The `#[NamespaceVisibility]` attribute has 2 optional arguments: + +#### excludeSubNamespaces option + +This is a boolean value. Its default value is false. +If set to true then calls to methods from sub namespaces are not allowed. +E.g. ```php namespace Foo { - #[Package] - class Mailer + class Telephone { - public function sendMessage(string $message): void + #[NamespaceVisibility(excludeSubNamespaces: true)] + public function ring(): void { - // Some implementation - } + } } + } -namespace Bar { +namespace Foo\SubNamespace { - class PdfSender + use Foo\Telephone; + + class SubNamespaceRinger { - public function __invoke(Mailer $mailer): void + public function ring(Telephone $telephone): Person { - // ERROR: The method Mailer::sendMessage is on a package level class. - $mailer->sendMessage("some message"); + $telephone->ring(); // ERROR - Not allowed to call Telephone::ring() from a sub namespace } } - } ``` -**NOTES:** - -- If adding the `#[Package]` to a method, this method MUST have public visibility. -- If a class is marked with `#[Package]` then all its public methods are treated as having package visibility. -- This is currently limited to method calls (including `__construct`). -- Namespaces must match exactly. E.g. a package level method in `Foo\Bar` is only accessible from `Foo\Bar`. It is not accessible from `Foo` or `Foo\Bar\Baz` - +#### namespace option -## Friend +This is a string or null value. Its default value is null. +If it is set, then this is the namespace that you are allowed to call the method on. -A method or class can supply via a `#[Friend]` attribute a list of classes, they are friends with. Only their friend's classes may call the method. -Friendship is not reciprocated, e.g. if Dog makes Cat a friend, this does not mean that Cat considers Dog a friend. -This is loosely based on C++ friend feature. +In the example below you can only call `Telephone::ring` from the `Bar` namespace. -Example: ```php +namespace Foo { -class Person -{ - #[Friend(PersonBuilder::class)] - public function __construct() + class Telephone + { + #[NamespaceVisibility(namespace: "Bar")] + public function ring(): void { - // Some implementation } -} - - -class PersonBuilder -{ - public function build(): Person + } + + class Ringer + { + public function ring(Telephone $telephone): void { - $person = new Person(): // OK PersonBuilder is a friend of Person - // set up Person - return $person; + $telephone->ring(); // ERROR - Can only all Telephone::ring() from namespace Bar } + } } +namespace Bar { -// ERROR Call to Person::__construct is not from PersonBuilder -$person = new Person(); - -``` - -**NOTES:** -- Multiple friends can be specified. E.g. `#[Friend(Foo::class, Bar::class)]` -- A class can have a `#[Friend]` attribute. Friendship is additive. E.g. - ```php - #[Friend(Foo::class)] - class Entity + use Foo\Telephone; + + class AnotherRinger { - #[Friend(Bar::class)] - public function ping(): void // ping is friends with Foo and Bar + public function ring(Telephone $telephone): void { + $telephone->ring(); // OK - Allowed to call Telephone::ring() from namespace Bar } } - ``` -- This is currently limited to method calls (including `__construct`). - -## Sealed - -**This attribute is a work in progress** +} +``` -This replicates the rejected [sealed classes RFC](https://wiki.php.net/rfc/sealed_classes) +#### NamespaceVisibility on classes -The `#[Sealed]` attribute takes a list of classes or interfaces that can extend/implement the class/interface. +If a class was the `#[NamespaceVisibility]` Attribute, then all its public methods are treated as Namespace visibility. E.g. ```php +namespace Foo { -#[Sealed(Success::class, Failure::class)] -abstract class Result {} // Result can only be extended by Success or Failure - -// OK -class Success extends Result {} - -// OK -class Failure extends Result {} - -// ERROR AnotherClass is not allowed to extend Result -class AnotherClass extends Result {} + #[NamespaceVisibility()] + class Telephone + { + public function ring(): void // This method has NamespaceVisibility + { } + } +} ``` -## TestTag - -The `#[TestTag]` attribute is an idea borrowed from hardware testing. Methods marked with this attribute are only available to test code. - -E.g. +If both the class and one of the class's methods has a `#[NamespaceVisibility]` attribute, then the method's attribute +takes precedence. ```php -class Person { - - #[TestTag] - public function setId(int $id) - { - $this->id = $id; - } -} - +namespace Foo { -function updatePersonId(Person $person): void -{ - $person->setId(10); // ERROR - not test code. + #[NamespaceVisibility(namespace: 'Bar')] + class Telephone + { + #[NamespaceVisibility(namespace: 'Baz')] + public function ring(): void // This method can only be called from the namespace Baz + { } + } } +``` -class PersonTest -{ - public function setup(): void - { - $person = new Person(); - $person->setId(10); // OK - This is test code. - } -} -``` +#### NOTES: -NOTES: -- Methods with the`#[TestTag]` MUST have public visibility. -- 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: - - 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). - - Assume all classes within a namespace is test code. See [namespace config option](https://github.com/DaveLiddament/phpstan-php-language-extensions#exclude-checks-based-on-test-namespace). +- If adding the `#[NamespaceVisibility]` to a method, this method MUST have public visibility. +- This is currently limited to method calls (including `__construct`). ## InjectableVersion The `#[InjectableVersion]` is used in conjunction with dependency injection. -`#[InjectableVersion]` is applied to a class or interface. +`#[InjectableVersion]` is applied to a class or interface. It denotes that it is this version and not any classes that implement/extend that should be used in the codebase. E.g. @@ -416,6 +435,29 @@ NOTES: - 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). - 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). + +## Deprecated Attributes + +### Package (deprecated) + +The `#[Package]` attribute acts like an extra visibility modifier like `public`, `protected` and `private`. It is inspired by Java's `package` visibility modifier. +The `#[Package]` attribute limits the visibility of a class or method to only being accessible from code in the same namespace. + +This has been replaced by the `#[NamespaceVisibility]` attribute. To upgrade replace: + +`#[Package]` with `#[NamespaceVisibility(excludeSubNamespaces=true)]` + + +**NOTES:** + +- If adding the `#[Package]` to a method, this method MUST have public visibility. +- If a class is marked with `#[Package]` then all its public methods are treated as having package visibility. +- This is currently limited to method calls (including `__construct`). +- Namespaces must match exactly. E.g. a package level method in `Foo\Bar` is only accessible from `Foo\Bar`. It is not accessible from `Foo` or `Foo\Bar\Baz` + + + + ## Further examples More detailed examples of how to use attributes is found in [examples](examples/). diff --git a/examples/namespaceVisibility/namespaceVisibilityOnClass.php b/examples/namespaceVisibility/namespaceVisibilityOnClass.php new file mode 100644 index 0000000..c69e7dc --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityOnClass.php @@ -0,0 +1,64 @@ +updateName(); // OK: Calls to same class allowed + } + } + + class Updater + { + public function updater(Person $person): void + { + $person->updateName(); // OK: Calls within same namespace allowed + } + } + + $person = new Person(); + $person->updateName(); // OK: Calls within same namespace allowed + +} + + +namespace NamespaceVisibilityOnClass\SubNamesapce { + + use NamespaceVisibilityOnClass\Person; + + class AnotherClass + { + public function update(): void + { + $person = new Person(); + $person->updateName(); // OK: Calls within the same subnamespace allowed. + } + } +} + + +namespace NamespaceOnClass2 { + + use PackageOnClass\Person; + + class AnotherUpdater + { + public function update(): void + { + $person = new Person(); + $person->updateName(); // ERROR: Call to Person::update method which has namespace visibility. + } + } +} diff --git a/examples/namespaceVisibility/namespaceVisibilityOnClassExcludeSubNamespaces.php b/examples/namespaceVisibility/namespaceVisibilityOnClassExcludeSubNamespaces.php new file mode 100644 index 0000000..56b2aa3 --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityOnClassExcludeSubNamespaces.php @@ -0,0 +1,33 @@ +updateName(); // ERROR: Call to Person::updateName in a sub namespace, where sub namespace is not allowed. + } + } +} diff --git a/examples/namespaceVisibility/namespaceVisibilityOnClassForDifferentNamespaces.php b/examples/namespaceVisibility/namespaceVisibilityOnClassForDifferentNamespaces.php new file mode 100644 index 0000000..d7d894d --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityOnClassForDifferentNamespaces.php @@ -0,0 +1,33 @@ +updateName(); // OK: Call to Person::updateName is in the allowed namespace. + } + } +} diff --git a/examples/namespaceVisibility/namespaceVisibilityOnConstructor.php b/examples/namespaceVisibility/namespaceVisibilityOnConstructor.php new file mode 100644 index 0000000..fce4553 --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityOnConstructor.php @@ -0,0 +1,58 @@ +updateName(); // OK + } + } + + class Updater + { + public function updater(Person $person): void + { + $person->updateName(); // OK + } + } + + $person = new Person(); + $person->updateName(); // OK +} + + + +namespace NamespaceVisibilityOnMethod\SubNamespace { + + use NamespaceVisibilityOnMethod\Person; + class AnotherPersonUpdater + { + public function update(Person $person): void + { + $person->updateName(); // OK - Subnamespace of NamespaceVisibilityOnMethod, which is allowed + } + } +} + + +namespace NamespaceVisibilityOnMethod2 { + + use NamespaceVisibilityOnMethod\Person; + + class AnotherUpdater + { + public function update(): void + { + $person = new Person(); + $person->updateName(); // ERROR: Call to Person::updateName which has namespaceVisibility visibility + } + } +} diff --git a/examples/namespaceVisibility/namespaceVisibilityOnMethodExcludeSubNamespace.php b/examples/namespaceVisibility/namespaceVisibilityOnMethodExcludeSubNamespace.php new file mode 100644 index 0000000..8793ac4 --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityOnMethodExcludeSubNamespace.php @@ -0,0 +1,36 @@ +updateName(); // OK + } + } +} + + + +namespace NamespaceVisibilityOnMethodExluceSubNamespace\SubNamespace { + + use NamespaceVisibilityOnMethodExcludeSubNamespace\Person; + class AnotherPersonUpdater + { + public function update(Person $person): void + { + $person->updateName(); // ERROR - Subnamespace of NamespaceVisibilityOnMethod, which is not allowed + } + } +} + diff --git a/examples/namespaceVisibility/namespaceVisibilityOnMethodForDifferentNamespace.php b/examples/namespaceVisibility/namespaceVisibilityOnMethodForDifferentNamespace.php new file mode 100644 index 0000000..794a9f1 --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityOnMethodForDifferentNamespace.php @@ -0,0 +1,32 @@ +updateName(); // OK, call to Person::updateName from allowed namespace. + } + } +} + diff --git a/examples/namespaceVisibility/namespaceVisibilityOnStaticMethod.php b/examples/namespaceVisibility/namespaceVisibilityOnStaticMethod.php new file mode 100644 index 0000000..d664ae8 --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityOnStaticMethod.php @@ -0,0 +1,55 @@ +updateName(); // OK: calling from a class with a name ending Test + } + } + +} diff --git a/examples/namespaceVisibility/namespaceVisibilityRulesIgnoredForTestNamespace.php b/examples/namespaceVisibility/namespaceVisibilityRulesIgnoredForTestNamespace.php new file mode 100644 index 0000000..8dd5bf4 --- /dev/null +++ b/examples/namespaceVisibility/namespaceVisibilityRulesIgnoredForTestNamespace.php @@ -0,0 +1,33 @@ +updateName(); // OK: calling from a test namespace + } + } + +} diff --git a/src/NamespaceVisibility.php b/src/NamespaceVisibility.php new file mode 100644 index 0000000..a8bd128 --- /dev/null +++ b/src/NamespaceVisibility.php @@ -0,0 +1,17 @@ +