├── .gitignore ├── AttributesVersion.md ├── LICENSE.md ├── README.md ├── composer.json ├── example ├── attributes.php └── docblock.php ├── src ├── Extend.php ├── Implement.php ├── Template.php └── Type.php └── tests └── docblock ├── array_shapes.php ├── arrays.php ├── class_string.php ├── class_string_templates.php ├── edgecase_class_string_templates.php ├── extending_types.php ├── extending_types_with_restriction.php ├── extending_types_with_restriction_2.php ├── generator_full.php ├── generator_simple.php ├── namespace.php ├── restricting_templates.php ├── restricting_templates_2.php ├── restricting_templates_3.php ├── template_class_constructor.php ├── template_class_no_constructor.php ├── template_class_no_constructor_invalid.php ├── template_class_param.php ├── template_class_property.php ├── template_class_return.php ├── template_function.php └── template_multiple_types.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vagrant 3 | Vagrantfile 4 | 5 | vendor/ 6 | 7 | composer.lock 8 | -------------------------------------------------------------------------------- /AttributesVersion.md: -------------------------------------------------------------------------------- 1 | # PHP Generics specification (using Attributes) 2 | 3 | Developers might think that documenting the additional metadata required for generics should live in [Attributes](https://www.php.net/manual/en/language.attributes.overview.php) instead of a docblock. 4 | The documentation for attributes opens with this statement: 5 | 6 | > Attributes allow to add structured, machine-readable metadata information on declarations in code 7 | 8 | The metadata in an attribute is available: 9 | - via reflection at run time (not relevant for static analysis) 10 | - in the AST (use case for static anlaysers) 11 | 12 | This section has been included to propose a how attributes could be used for generics. 13 | 14 | Points to consider: 15 | 16 | - Not currently supported 17 | - Static analysis tool maintainers and vendors may have little desire to support this. (Initial feedback to the attributes proposal is not favourable.) 18 | - Is this actually better than the docblock version? 19 | 20 | That said if there is a desire to use attributes it might be the best method, other suggestions are examined [here](https://www.daveliddament.co.uk/articles/php-generics-standard/). 21 | 22 | Code using Attributes instead of docblock: 23 | ```php 24 | use StaticAnalysis\Generics\v1\Template; 25 | use StaticAnalysis\Generics\v1\Type; 26 | 27 | #[Template("T")] 28 | #[Type("array")] // This is documenting the return type 29 | function asArray( 30 | #[Type("T")] $value, 31 | ) { 32 | return [$value]; 33 | } 34 | ``` 35 | 36 | [More examples](example/attributes.php) of using Attributes. 37 | 38 | ## Using Attributes instead of docblocks 39 | 40 | There is already a move to use Attributes for metadata used by static analysers. For example PHPStorm has [added support](https://blog.jetbrains.com/phpstorm/2020/10/phpstorm-2020-3-eap-4/) Attributes such as `#[Immutable]` and `#[ArrayShape]`. 41 | 42 | PHP is in danger of having multiple standards for the same thing. 43 | Psalm's author [thoughts on PHP Attributes for static analysis](https://gist.github.com/muglug/03f63a0e6da1d95d03a014f374e8217d), points to Java where this issue has happened. 44 | Adding additional information for static analysis via attributes will happen. 45 | It would be better to do this as a single standard. 46 | 47 | Using Attributes has the benefit that multiple versions of the standard can exist. 48 | This allows a version 1 (compatible with the existing docblock standard) that covers the vast majority of generics. 49 | The standard can evolve to support more complex generics corner cases once those cases have been identified and a common ground has been reached. 50 | 51 | This standard proposed is to use the same standard as docblocks and put the information into attributes. 52 | Arguments for this method and reasons against are explored in the article [A standard for generics in PHP](https://www.daveliddament.co.uk/articles/php-generics-standard/). 53 | 54 | Four Attributes are proposed: 55 | 56 | - `#[Template]` [definition](src/Template.php) 57 | - `#[Type]` [definition](src/Type.php) 58 | - `#[Extend]` [definition](src/Extend.php) 59 | - `#[Implement]` [definition](src/Implement.php) 60 | 61 | Instead of `@template` docblock use the attribute `#[Template]`. 62 | For additional type information (that appears after `@var`, `@param` or `@return`) use the attribute `#[Type]`. 63 | 64 | ##### Param 65 | Using `@param` docblock: 66 | ```php 67 | /** 68 | * @param T $item 69 | */ 70 | public function add( 71 | $item, 72 | ): void { ... } 73 | ``` 74 | 75 | Using an attribute instead of `@param`: 76 | 77 | ```php 78 | public function add( 79 | #[Type("T")] $item, 80 | ): void { ... } 81 | ``` 82 | ##### Return 83 | 84 | Using the `@return` docblock: 85 | ```php 86 | /** 87 | * @return T 88 | */ 89 | public function next () { ... } 90 | ``` 91 | 92 | Using an attribute instead of `@return` (NOTE: it's not possible to add an attribute to a return type. However, as a function or method can only have one return type an attribute is attached to the function/method to give information about the return type) : 93 | 94 | ```php 95 | #[Type("T")] 96 | public function next() { ... } 97 | ``` 98 | 99 | 100 | ##### Template 101 | Using the `@template` docblock: 102 | ```php 103 | /** @template T */ 104 | class Queue { ... } 105 | ``` 106 | Same information as an attribute: 107 | ```php 108 | #[Template("T")] 109 | class Queue { ... } 110 | ``` 111 | 112 | ### Restricting template types in Attributes 113 | 114 | The `#[Template]` attribute takes an optional 2nd argument. The restriction of the template is the 2nd argument. 115 | 116 | Docblock version: 117 | 118 | ```php 119 | /** @template T of Animal */ 120 | interface AnimalGame { ... } 121 | ``` 122 | 123 | Attribute version: 124 | 125 | ```php 126 | #[Template("T", "Animal")] 127 | interface AnimalGame { ... } 128 | ``` 129 | 130 | 131 | 132 | 133 | ### Array and iterable differences 134 | 135 | Make array and iterable keys mandatory. I.e. REMOVE support for shortened versions `T[]`, `array` and `iterable`. 136 | 137 | So `#[Type("Person[]")]` must be `#[Type("array")]` 138 | 139 | And `#[Type("array")]` must be `#[Type("array")]` 140 | 141 | 142 | ### Resolving class names in Attributes 143 | 144 | 145 | When class names are used in generics attributes the rules for resolving them are the same as they are for normal PHP code. 146 | 147 | ```php 148 | namespace Code; 149 | 150 | use Entities\Student; 151 | 152 | class Room {...} 153 | 154 | // Room is defined in same namespace 155 | #[Type("array")] 156 | function getRooms(): array {...} 157 | 158 | // Student is included via use statement 159 | #[Type("array")] 160 | function getStudents(): array {...} 161 | 162 | // FQCN for Subject is used 163 | #[Type("array")] 164 | function getSubjects(): array {...} 165 | ``` 166 | 167 | ### Extending/Implementing types in Attributes 168 | 169 | `extends` is a reserved word in PHP. An attribute called `extends` is not allowed. Instead use `#[Extend]`. 170 | 171 | `implements` is a reserved word in PHP. An attribute called `implements` is not allowed. Instead use `#[Implement]`. 172 | 173 | 174 | ## Feedback 175 | 176 | Raise issues or create a PR with proposal for improvements. 177 | 178 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Dave Liddament 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Generics specification (for static analysis) 2 | 3 | > This is very much work in progress. As of 29th Jan 2021 trying to determine if there is a need for this and whether now is the time. See and comment on [Is a standard needed?](https://github.com/DaveLiddament/php-generics-standard/issues/2) 4 | > 5 | > If you see any problems or have and comments then please raise issues or submit small PRs to enhance the document. 6 | 7 | Generics in PHP are already a reality by using advanced static analysers such as 8 | [Psalm](https://psalm.dev) and [PHPStan](https://phpstan.org/). 9 | There are huge benefits that added type safety that generics bring. 10 | These benefits are due to improved clarity of code and reduced costs from fewer bugs. 11 | 12 | There is already an unofficial standard for generics, see documentation from 13 | [Psalm](https://psalm.dev/docs/annotating_code/templated_annotations/) and 14 | [PHPStan](https://medium.com/@ondrejmirtes/generics-in-php-using-phpdocs-14e7301953). 15 | Additional information required for generics is added in docblocks. 16 | 17 | A major blocker to increased uptake is the lack of an "official" standard for generics. 18 | A standard will provide tools (such as IDEs) and libraries with a clear guidelines for implementing and supporting generics. 19 | 20 | The purposes of this repository are: 21 | - Formalise the existing unofficial standards by specifying the syntax. (Rest of this document) 22 | - Create a series of test set of code snippets for testing static analysers against the specification. (See `tests`) 23 | - Eventually progress to a PSR or similar "official" standard. (Assuming this is something the FIG would support). 24 | 25 | 26 | #### Goals 27 | 28 | - To create a clear set of standards for annotating code with the additional information required for generics. Analysis is done by static analysers, not at the run time. 29 | - Provide a set of code samples that illustrate correct behaviour for generics. 30 | - The initial standard is pragmatic. It will aim to address the vast majority of use cases. Some edge cases will not be addressed. 31 | - The standard will not prevent code from working that does not support the generics notation. 32 | - Has buy in from the established static analysers (Psalm, PHPStan and Phan) and IDEs (PHPStorm, see their [initial technical feedback](https://github.com/DaveLiddament/php-generics-standard/issues/5)). 33 | - Will be palatable for library and framework maintainers to add support if they want to. 34 | 35 | #### Non Goals 36 | 37 | - Run time support. The information is for static analysers only. 38 | - To provide complete generics support. 39 | - This deals with only docblocks required for generics. This is not a replacement for PSRs [5](https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc.md) and [19](https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc-tags.md). 40 | 41 | 42 | 43 | ### Overview 44 | 45 | There are two parts to the specification. 46 | 47 | 1. The syntax itself (which is based on the current unofficial standard). 48 | 1. How code is annotated with the additional information required by generics. Proposed are 2 methods: 49 | - Docblocks (the current unofficial standard). 50 | - Subject to demand and support from tools an Attributes. This the same syntax but adding it to an attribute rather than a docblock. 51 | 52 | The specification is supported by a series of code snippets to illustrate the expected behaviour (with respect to generics). 53 | 54 | This is a brief examples of how code can be annotated with additional information required for generics. 55 | 56 | 57 | #### Docblock version 58 | This is the current unofficial standard: 59 | 60 | ```php 61 | /** 62 | * @template T 63 | * @param T $value 64 | * @return array 65 | */ 66 | function asArray( 67 | $value, 68 | ) { 69 | return [$value]; 70 | } 71 | ``` 72 | 73 | #### Attribute version 74 | 75 | The attribute version is documented in [AttributeVersion.md](AttributesVersion.md). 76 | 77 | ## Contents 78 | 79 | - This file: 80 | - [TODO](#todo) 81 | - [FAQs](#faqs) 82 | - [Related articles](#related-articles) 83 | - [PHP Generics Notation](#php-generics) 84 | - [Attributes](#using-attributes-instead-of-docblocks) 85 | - [Test cases](#test) 86 | - [Further discussion points](#further-discussion-points) 87 | - [Conclusions](#conclusions) 88 | - [Feedback, suggestions, comments](#feedback) 89 | 90 | - Attributes (PHP Code) (Assuming there is demand and support for this): 91 | - [Template](src/Template.php) 92 | - [Type](src/Type.php) 93 | - [Extend](src/Extend.php) 94 | - [Implement](src/Implement.php) 95 | 96 | - Code Examples: 97 | - [Using docblocks](example/docblock.php) 98 | - [Using Attributes](example/attributes.php) 99 | 100 | - Test cases: 101 | - [Test cases](tests) 102 | 103 | 104 | 105 | ## TODO 106 | 107 | - [ ] Add more test cases (e.g. corner cases) 108 | - [ ] Add test cases for Attributes (port the docblock tests, maybe a job for Rector?) 109 | - [ ] Add glossary 110 | - [ ] Create script to run test code samples through Psalm and PHPStan and check errors reported by those tools match lines annotated with `ERROR:` in the sample files. 111 | - [ ] Add code of conduct 112 | - [ ] Add contributing doc 113 | 114 | ## FAQs 115 | 116 | - **Did you know X is already working on this?** 117 | No. Let [Dave Liddament](https://twitter.com/daveliddament) know and we'll join forces. 118 | - **Why isn't this a PSR?** 119 | The hope is this will become a PSR or similar. The purpose of this document is to get the process doing. If there is enough interest then it will be submitted to the FIG in the hope it becomes a PSR. 120 | - **Isn't this covered by PSRs [5](https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc.md) and [19](https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc-tags.md)?** 121 | Those are more general PSRs. Here the focus is only on generics. 122 | - **Who are you to decide this?** 123 | Merely an enthusiastic user of static analysis tools and a fan of generics (from Java days). The hope is this will help wider adoption of generics and static analysis in the PHP community. 124 | - **What happens if PHP evolves to have generics as part of the language?** 125 | That will be great news. Tools like [Rector](https://getrector.org/) will have rulesets created to automatically convert from code annotated with generics information to full language support. 126 | 127 | 128 | ## Related articles 129 | 130 | - [Psalm: Templated annotations](https://psalm.dev/docs/annotating_code/templated_annotations/) 131 | - [PHPStan: Generics in PHP using PHPDocs](https://medium.com/@ondrejmirtes/generics-in-php-using-phpdocs-14e7301953) 132 | - [A standard for generics in PHP](https://www.daveliddament.co.uk/articles/php-generics-standard/) 133 | - [Psalm's author's thoughts on PHP Attributes for static analysis](https://gist.github.com/muglug/03f63a0e6da1d95d03a014f374e8217d) 134 | 135 | 136 | ## PHP Generics 137 | 138 | Generics requires additional information. This additional information is added either via docblock or Attribute. 139 | 140 | 141 | ## Glossary 142 | 143 | > TODO add in definition of terms including: supertype, subtype, covariance, contravariance, union types, etc 144 | 145 | ### Template type 146 | 147 | Code is often written that can operate on data of any types. Consider code that models a queue. 148 | The queue could hold almost anything; strings, objects, integers, etc. 149 | At the time of writing the code the type of data the queue holds is unknown. 150 | Instead of specifying the type a placeholder or _template type_ is specified instead. 151 | The actual type that the _templated type_ resolves to is known based on the context of how the code is used. 152 | 153 | A _templated type_ MUST resolve to any FQCN or primitive type. 154 | 155 | By convention a _templated type_ is often referred to as `T`. 156 | In the case of arrays or collections with keys and values by convention `K` and `V` are used. 157 | 158 | Using docblocks the _template type_ is defined using `@template`, e.g. `@template T` 159 | 160 | The `@template` annotation can only be added to: 161 | - functions 162 | - methods 163 | - classes 164 | 165 | 166 | #### Class 167 | 168 | Here is a class that holds a value, the type of the value it holds, `T`, is not known at the time of writing the `ValueHolder` class. 169 | 170 | ```php 171 | /** 172 | * @template T 173 | */ 174 | class ValueHolder 175 | { 176 | /** @var T */ 177 | private $value; 178 | 179 | /** @param T $value */ 180 | public function __construct($value) 181 | { 182 | $this->value = $value; 183 | } 184 | 185 | /** @return T */ 186 | public function value() 187 | { 188 | return $this->value; 189 | } 190 | } 191 | ``` 192 | 193 | There are 3 ways of specifying the type of `T`. 194 | 195 | ##### Constructor 196 | 197 | The first is via the type passed into the constructor. 198 | In the example below an `int` is passed into the constructor. In this context `T` is an `int`. 199 | 200 | ```php 201 | $valueHolder = new ValueHolder(21); 202 | 203 | $age = $valueHolder->value(); // $age is of type int. 204 | ``` 205 | 206 | ###### Examples: 207 | - [tests/docblock/template_class_constructor.php](tests/docblock/template_class_constructor.php) 208 | 209 | ##### Return 210 | The second to specify the type is giving type information is via a `@return`. 211 | 212 | ```php 213 | /** @return ValueHolder */ 214 | function getAgeValueHolder(): ValueHolder 215 | { 216 | return new ValueHolder(21); 217 | } 218 | 219 | $age = getAgeValueHolder()->value(); // $age is of type int 220 | ``` 221 | 222 | ###### Examples: 223 | - [tests/docblock/template_class_return.php](tests/docblock/template_class_return.php) 224 | 225 | 226 | ##### Property 227 | 228 | The third method is to specify type information via a `@var` docblock on a class property. 229 | 230 | ```php 231 | class Entity 232 | { 233 | /** @var ValueHolder */ 234 | public ValueHolder $ageValueHolder; 235 | 236 | public function __construct() 237 | { 238 | $this->ageValueHolder = new ValueHolder(21); 239 | } 240 | } 241 | 242 | $entity = new Entity(); 243 | $age = $entity->ageValueHolder; // $age is of type int 244 | ``` 245 | 246 | ###### Examples: 247 | - [tests/docblock/template_class_property.php](tests/docblock/template_class_property.php) 248 | 249 | ##### Param 250 | 251 | The final method is specifying type information via a `@param` docblock. 252 | 253 | ```php 254 | /** @param ValueHolder $intValueHolder */ 255 | function takesIntValueHolder(ValueHolder $intValueHolder): void 256 | { 257 | $age = $intValueHolder->value(); // $age is of type int 258 | } 259 | ``` 260 | 261 | ###### Examples: 262 | - [tests/docblock/template_class_param.php](tests/docblock/template_class_param.php) 263 | 264 | 265 | #### Class templates that cannot be derived from constructor 266 | 267 | Consider an object that models a queue: 268 | 269 | ```php 270 | /** @template T */ 271 | class Queue 272 | { 273 | /** T $item */ 274 | public function add($item): void {...} 275 | 276 | /** @return T */ 277 | public function next() {...} 278 | } 279 | ``` 280 | 281 | When creating an instance of the Queue the type `T` can not be inferred. 282 | 283 | The type of entities in the queue needs explicitly stating. In the example below the `@var` docblock is used to show the `Queue` takes items of type `string`: 284 | 285 | ```php 286 | /** @var Queue $queue */ 287 | $queue = new Queue(); 288 | 289 | $queue->add("hello"); // This is OK 290 | $item = $queue->next(); // $item is a string 291 | ``` 292 | 293 | ###### Examples: 294 | - [tests/docblock/template_class_no_constructor.php](tests/docblock/template_class_no_constructor.php) 295 | - [tests/docblock/template_class_no_constructor_invalid.php](tests/docblock/template_class_no_constructor_invalid.php) 296 | 297 | 298 | #### Function 299 | 300 | Template types can also be used on functions. E.g.: 301 | 302 | ```php 303 | /** 304 | * @template T 305 | * @param T $value 306 | * @return T 307 | */ 308 | function mirror($value) 309 | { 310 | return $value; 311 | } 312 | ``` 313 | In this example the type `T` is determined by the type of the argument `$value`. 314 | 315 | In the example below `$value` is of type string. Therefore `T` will be `string`. The return type and thus `$mirroredValue` will also be of type `string`. 316 | 317 | ```php 318 | $mirroredValue = mirror("hello world"); 319 | ``` 320 | 321 | ###### Examples: 322 | - [tests/docblock/template_function.php](tests/docblock/template_function.php) 323 | 324 | 325 | 326 | ### Multiple types 327 | 328 | It is possible to specify multiple types. Consider a code to represent a map collection. The type of both the map key (K) and map value (V) need specifying: 329 | 330 | ```php 331 | /** 332 | * @template K 333 | * @template V 334 | */ 335 | class Map 336 | { 337 | /** 338 | * @param K $key 339 | * @param V $value 340 | */ 341 | public function add($key, $value): void {...} 342 | 343 | /** 344 | * @param K $key 345 | * @return V 346 | */ 347 | public function getValue($key) {...} 348 | } 349 | ``` 350 | 351 | To specify multiple _templated types_ add the type information in the angular brackets in the same order that the `@template` appear. 352 | In the `Map` example, the first `@template` is for the type of `K` and the second for `V`. 353 | In the following example `K` is `string` and `V` is `Person`: 354 | 355 | ```php 356 | /** @var Map */ 357 | $map = new Map(); 358 | ``` 359 | 360 | ###### Examples: 361 | - [tests/docblock/template_multiple_types.php](tests/docblock/template_multiple_types.php) 362 | 363 | ### Restricting Template types 364 | 365 | It is possible to restrict what a _template type_ resolves to. For example restricting `T` to only be objects is done by using `of`: 366 | 367 | ```php 368 | /** @template T of object */ 369 | ``` 370 | 371 | Full of example: 372 | 373 | ```php 374 | /** 375 | * @template T of object 376 | * @param T $value 377 | * @return T 378 | */ 379 | function mirror($value) 380 | { 381 | return $value; 382 | } 383 | 384 | 385 | $person = mirror(new Person); // OK 386 | 387 | $int = mirror(7); // Problem as int is not an object 388 | ``` 389 | 390 | 391 | It is also possible a number of valid types. E.g. to allow `T` to be either an `int` of `string` is done like this: 392 | 393 | ```php 394 | /** @template T of int|string */ 395 | ``` 396 | 397 | Example: 398 | 399 | ```php 400 | /** 401 | * @template T of int|string 402 | * @param T $value 403 | * @return T 404 | */ 405 | function mirror($value) 406 | { 407 | return $value; 408 | } 409 | 410 | $int = mirror(7); // OK 411 | $bool = mirror(true); // Problem as a boolean is not a string or int. 412 | ``` 413 | 414 | A template can restrict to an object and subtypes of that object. For example: 415 | 416 | ```php 417 | interface Shape {...} 418 | 419 | class Square implements Shape {...} 420 | 421 | /** @template T of Shape */ 422 | class ShapeProcessor {...} // T can only resolve to Shape or a subtype of Shape 423 | 424 | /** @var ShapeProcessor $shapeProcessor */ 425 | $shapeProcessor = new ShapeProcessor(); // OK - Shape is a Shape! 426 | 427 | /** @var ShapeProcessor $squareProcessor */ 428 | $squareProcessor = new ShapeProcessor(); // OK - Square is a Shape 429 | 430 | /** @var ShapeProcessor $personProcessor */ 431 | $personProcessor = new ShapeProcessor(); // ERROR: Person not subtype of Shape 432 | ``` 433 | 434 | ###### Examples: 435 | - [tests/docblock/restricting_templates.php](tests/docblock/restricting_templates.php) 436 | - [tests/docblock/restricting_templates_2.php](tests/docblock/restricting_templates_2.php) 437 | - [tests/docblock/restricting_templates_3.php](tests/docblock/restricting_templates_3.php) 438 | 439 | 440 | ### Class string 441 | 442 | A `class-string` is a string that represents the FQCN of a class. 443 | 444 | ```php 445 | /** 446 | * @param class-string $className 447 | */ 448 | function takesClassString(string $className): void {} 449 | 450 | takesClassString(Person::class); // OK (assuming Person is a valid class) 451 | 452 | takesClassString("a random string"); // ERROR: Does not represent FQCN 453 | ``` 454 | 455 | A class string can be used in conjunction with a _templated type_. 456 | 457 | In the example below `$className` is the FQCN of the type `T`, so `T` is of type `Person`: 458 | 459 | ```php 460 | /** 461 | * @template T 462 | * @param class-string $className 463 | * @return T 464 | */ 465 | function build(string $className) { 466 | return new $className; 467 | } 468 | 469 | $person = build(Person::class); // $person is an object of type Person 470 | ``` 471 | 472 | ###### Examples: 473 | - [tests/docblock/class_string.php](tests/docblock/class_string.php) 474 | - [tests/docblock/template_class_string.php](tests/docblock/template_class_string.php) 475 | 476 | 477 | ### Extending/Implementing types 478 | 479 | 480 | #### Extends 481 | Consider a repository. The base class has a `persist` method. 482 | 483 | ```php 484 | /** @template T */ 485 | abstract class Repository 486 | { 487 | /** @param T $entity */ 488 | public function persist($entity) {...} 489 | } 490 | ``` 491 | 492 | The concrete implementations must specify the `T` and could provide additional methods. E.g.: 493 | ```php 494 | /** @extends Repository */ 495 | class PersonRepository extends Repository {...} 496 | ``` 497 | 498 | NOTE: the `@extends` docblock. It states that `Repository` is being extended. It also states that `T` is of type `Person`. 499 | 500 | 501 | #### Implements 502 | 503 | If a class is implementing and interface then use `@implements`. 504 | 505 | ```php 506 | interface Job {...} 507 | class SendEmailJob implements Job {...} 508 | class CreatePdfJob implements Job {...} 509 | 510 | /** @template T */ 511 | interface JobProcessor 512 | { 513 | /** @param T $job */ 514 | public function process($job): void {...} 515 | } 516 | 517 | /** @implements JobProcessor */ 518 | class EmailSenderJobProcessor implements JobProcessor 519 | { 520 | public function process($job): void {...} 521 | } 522 | 523 | $emailSenderJobProcessor = new EmailSenderJobProcessor(); 524 | $emailSenderJobProcessor->process(new SendEmailJob()); // OK 525 | 526 | $emailSenderJobProcessor->process(new CreatePdfJob()); // ERROR. Expected SendEmailJob got CreatePdfJob 527 | ``` 528 | 529 | #### Restricting extended/implemented types 530 | 531 | As before it is possible to put restrictions on the templated type. E.g. `T` in `JobProcessor` should be restricted to `Job`. 532 | This is done as before: 533 | 534 | ```php 535 | /** @template T of Job */ 536 | interface JobProcessor {...} 537 | 538 | class Person {} 539 | 540 | // The following is not allowed as Person is not a Job 541 | /** @implements JobProcessor */ 542 | class PersonProcessor implements JobProcessor {...} 543 | ``` 544 | 545 | ###### Examples: 546 | - [tests/docblock/extending_types.php](tests/docblock/extending_types.php) 547 | - [tests/docblock/extending_types_with_restriction.php](tests/docblock/extending_types_with_restriction.php) 548 | - [tests/docblock/extending_types_with_restriction_2.php](tests/docblock/extending_types_with_restriction_2.php) 549 | 550 | 551 | ### Arrays and iterables 552 | 553 | > TODO behaviour difference between PHPStan and Psalm. Need to decide correct path to take here. 554 | 555 | #### array and iterable 556 | 557 | Arrays and iterables can have their key and value pairs specified, just as with generics. E.g. 558 | 559 | ```php 560 | /** 561 | * @param iterable $people 562 | * @return array 563 | */ 564 | function sortPeople(iterable $people): array {} 565 | ``` 566 | 567 | Short versions that don't specify the type of the key are also allowed: 568 | 569 | ```php 570 | /** 571 | * @param iterable $people 572 | * @return array 573 | */ 574 | function sortPeople(iterable $people): array {} 575 | ``` 576 | In the cases above the type of key is assumed to be `int|string`. This means `array` is treated as `array`. 577 | 578 | #### Type[] 579 | 580 | A frequently used convention for specifying returning and array of things (e.g. Books) is: 581 | 582 | ```php 583 | /** @return Book[] */ 584 | function getBooks() {...} 585 | ``` 586 | 587 | `Book[]` is the treated as `array` 588 | 589 | Or more generally: 590 | 591 | `T` is the same as `array` 592 | 593 | ###### Examples: 594 | - [tests/docblock/arrays.php](tests/docblock/arrays.php) 595 | 596 | > TODO lots more test cases needed here 597 | 598 | ### Array shapes 599 | 600 | Support for object like arrays is documented in this way: 601 | 602 | ```array{0: string, person: Person, age?: int}``` 603 | 604 | This means: 605 | 606 | - The first item in the array must be of type `string`. 607 | - An entry with the key `person` and value of type `Person` object MUST be supplied. 608 | - An optional entry with key `age` and value of type `int` can also be specified. The `?` after the key name denotes it is optional. 609 | 610 | Example 611 | 612 | ```php 613 | takesArrayShape(['Anna', 'age' => 21, 'person' => new Person()]); // OK - All data provided 614 | takesArrayShape(['Bob', 'person' => new Person()]); // OK - All all mandatory data provided 615 | takesArrayShape([true, 'age' => 21, 'person' => new Person()]); // ERROR: Wrong type for arg 0. 616 | takesArrayShape(['Charlie', 'age' => 22]); // ERROR: Missing 'Person' 617 | 618 | /** @param array{0: string, age?:int, person:Person} $array */ 619 | function takesArrayShape(array $array): void {..} 620 | ``` 621 | 622 | 623 | ###### Examples: 624 | - [tests/docblock/array_shapes.php](tests/docblock/array_shapes.php) 625 | 626 | 627 | 628 | ### Generators 629 | 630 | Generators can be provided with type information for key, value, send and return types. 631 | 632 | The first type is for key. The second for value. Third for send type. Forth for return type. 633 | 634 | ```php 635 | /** @return Generator */ 636 | function getPeople(): Generator {...} 637 | 638 | foreach(getPeople() as $name => $person) { 639 | // $name is of type string 640 | // $person is of type Person 641 | 642 | getPeople()->send(true); // Type sent must be of type bool 643 | } 644 | 645 | $count = getPeople()->getReturn(); // $count is of type int 646 | ``` 647 | 648 | When providing types either key and value must be provided, or all 4 types must be provided. 649 | 650 | ###### Examples: 651 | - [tests/docblock/generator_simple.php](tests/docblock/generator_simple.php) 652 | - [tests/docblock/generator_full.php](tests/docblock/generator_full.php) 653 | 654 | 655 | 656 | ### Types 657 | 658 | > TODO Decide which types MUST be supported 659 | 660 | Examples: 661 | - `array-key` (alias for `string|int`) 662 | - `callable-array` 663 | 664 | 665 | See full list from [Psalm](https://psalm.dev/docs/annotating_code/type_syntax/atomic_types/) and [PHPStan](https://phpstan.org/writing-php-code/phpdoc-types). 666 | 667 | Remember the scope of this specification is just for generics, need to strike the balance between just supporting generics, 668 | but also not hindering projects static analysis that has more specialised types (e.g. `numeric`). 669 | Perhaps a separate specification is needed for aliases? 670 | 671 | 672 | ### Resolving class names 673 | 674 | When class names are used in generics docblocks the rules for resolving them are the same as they are for normal PHP code. 675 | 676 | ```php 677 | namespace Code; 678 | 679 | use Entities\Student; 680 | 681 | class Room {...} 682 | 683 | /** @var array */ 684 | $rooms = []; // Room is defined in same namespace 685 | 686 | /** @var array */ 687 | $students = []; // Student is included via use statement 688 | 689 | /** @var array */ 690 | $subjects = []; // FQCN for Subject is used 691 | ``` 692 | 693 | ###### Examples: 694 | - [tests/docblock/namespace.php](tests/docblock/namespace.php) 695 | 696 | 697 | 698 | ## Further discussion points 699 | 700 | Code samples showing edge cases where PHPStan and Psalm differ. 701 | 702 | - [tests/docblock/edgecase_class_string_templates.php](tests/docblock/edgecase_class_string_templates.php) 703 | 704 | 705 | ## Tests 706 | 707 | Tests provide an essential part of this standard. 708 | They show a static analyser should interpret code. They also define the correct behaviour for many of the corner cases that appear in generics. 709 | 710 | The tests are available under the [tests](tests/) folder. 711 | 712 | 713 | ### Rules for test scripts 714 | Each script under the `tests` folder MUST be analysed on its own. 715 | 716 | Each script should focus on one concept. 717 | 718 | Concepts SHOULD have both passing and failing examples. 719 | 720 | Happy path examples MUST have the comment `// OK` (There can be optional additional information as to why the case is valid) 721 | Failing examples MUST have the comment `// ERROR - ` 722 | 723 | The `// OK` or `// ERROR` comments MUST be on the same line of code. (I.e. it can not be before or after). 724 | This is so these scripts can be used as automated testing. 725 | 726 | If a line of code has an `// OK` or `// ERROR` comment then it MUST NOT be split over multiple lines. (This is to help with test automation). 727 | 728 | To test data is a certain type use a `takesX` function, e.g. `function takesInt(int $value): void` 729 | 730 | ### Example: 731 | 732 | ```php 733 | /** 734 | * @template T 735 | * @param T $value 736 | * @return T 737 | */ 738 | function mirror($value) 739 | { 740 | return $value; 741 | } 742 | 743 | function takesString(string $value): void {} 744 | function takesInt(int $value): void {} 745 | 746 | $stringValue = mirror("hello"); 747 | takesString($stringValue); // OK 748 | takesInt($stringValue); // ERROR. Method expects int, string given 749 | ``` 750 | 751 | NOTE: Warnings/errors that are not applicable to generics MUST be ignored. 752 | E.g. warnings about unused variables are not relevant to generics, so MUST be ingored. 753 | 754 | ## Conclusions 755 | 756 | There is already a widely used unofficial standard for annotating code to enable static analysis for generics. 757 | This proposal endorses the existing standard. 758 | 759 | There is an [additional proposal](AttributesVersion.md) that uses Attributes for annotating code with the extra information required for generics. 760 | 761 | 762 | Acting now to formalise a version 1 of generics will stop multiple tools and vendors implementing the same thing. 763 | It will provide a standard that all static analysers and libraries can follow. 764 | This will provide maximum benefit to the PHP ecosystem. 765 | 766 | 767 | ## Feedback 768 | 769 | Raise issues or create a PR with proposal for improvements. 770 | 771 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daveliddament/generics", 3 | "description": "Attributes for holding information about generics", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Dave Liddament", 9 | "email": "dave@lampbristol.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "StaticAnalysis\\Generics\\v1\\" : "src" 15 | } 16 | }, 17 | "require": {} 18 | } 19 | -------------------------------------------------------------------------------- /example/attributes.php: -------------------------------------------------------------------------------- 1 | ")] 26 | class DogGame implements AnimalGame { 27 | 28 | public function play($animal): void 29 | { 30 | $animal->bark(); 31 | } 32 | } 33 | 34 | // Given 35 | $dog = new Dog(); 36 | $cat = new Cat(); 37 | $dogGame = new DogGame(); 38 | 39 | 40 | // This is correct 41 | $dogGame->play($dog); 42 | $cat = new Cat(); 43 | $dogGame = new DogGame(); 44 | 45 | 46 | // This is correct 47 | $dogGame->play($dog); 48 | 49 | // This should be picked up as an error by static analysis 50 | $dogGame->play($cat); 51 | 52 | 53 | interface Car {} 54 | 55 | // This should also be picked up as an error by static analysis 56 | #[Implement("AnimalGame")] 57 | class CarGame implements AnimalGame 58 | { 59 | public function play($animal): void {} 60 | } 61 | 62 | -------------------------------------------------------------------------------- /example/docblock.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class DogGame implements AnimalGame 31 | { 32 | public function play($animal): void 33 | { 34 | $animal->bark(); 35 | } 36 | } 37 | 38 | // Given 39 | $dog = new Dog(); 40 | $cat = new Cat(); 41 | $dogGame = new DogGame(); 42 | 43 | 44 | // This is correct 45 | $dogGame->play($dog); 46 | 47 | // ERROR: This should be picked up as an error by static analysis 48 | $dogGame->play($cat); 49 | 50 | 51 | interface Car {} 52 | 53 | // ERROR: This should also be picked up as an error by static analysis 54 | /** @implements AnimalGame */ 55 | class CarGame implements AnimalGame 56 | { 57 | public function play($animal): void {} 58 | } 59 | -------------------------------------------------------------------------------- /src/Extend.php: -------------------------------------------------------------------------------- 1 | 21, 'person' => new Person()]); // OK - All data provided 8 | takesArrayShape(['Bob', 'person' => new Person()]); // OK - All all mandatory data provided 9 | takesArrayShape([true, 'age' => 21, 'person' => new Person()]); // ERROR: Wrong type for arg 0. 10 | takesArrayShape(['Charlie', 'age' => 22]); // ERROR: Missing 'Person' 11 | takesArrayShape(['Bob', 'person' => new Person(), 'address' => 'Some street']); // OK - All mandatory data provided, additional fields can also be supplied 12 | 13 | /** @param array{0: string, age?:int, person:Person} $array */ 14 | function takesArrayShape(array $array): void {} 15 | -------------------------------------------------------------------------------- /tests/docblock/arrays.php: -------------------------------------------------------------------------------- 1 | $array */ 19 | function takesValueTypeOnlyArray(array $array): void {} 20 | 21 | /** @param array $array */ 22 | function takesIntKeyAndValueArray(array $array): void {} 23 | 24 | /** @param array $array */ 25 | function takesStringKeyAndValueArray(array $array): void {} 26 | 27 | 28 | /** @return Person[] */ 29 | function providesShortSyntax() 30 | { 31 | return [new Person()]; 32 | } 33 | 34 | $shortSyntaxArray = providesShortSyntax(); 35 | takesAnyArray($shortSyntaxArray); // OK 36 | takesValueTypeOnlyArray($shortSyntaxArray); // OK 37 | takesIntKeyAndValueArray($shortSyntaxArray); // ERROR: Array key is not guaranteed int 38 | takesStringKeyAndValueArray($shortSyntaxArray); // ERROR: Array key is not guaranteed string 39 | 40 | foreach($shortSyntaxArray as $key => $value) { 41 | takesPerson($value); // OK 42 | takesArrayKey($key); // OK 43 | takesString($key); // ERROR: Array key is not guaranteed int 44 | takesInt($key); // ERROR: Array key is not guaranteed string 45 | } 46 | 47 | 48 | /** @return array */ 49 | function providesValueOnly() 50 | { 51 | return [new Person()]; 52 | } 53 | 54 | $valueOnlyArray = providesValueOnly(); 55 | takesAnyArray($valueOnlyArray); // OK 56 | takesValueTypeOnlyArray($valueOnlyArray); // OK 57 | takesIntKeyAndValueArray($valueOnlyArray); // ERROR: Array key is not guaranteed int 58 | takesStringKeyAndValueArray($valueOnlyArray); // ERROR: Array key is not guaranteed string 59 | 60 | foreach($valueOnlyArray as $key => $value) { 61 | takesPerson($value); // OK 62 | takesArrayKey($key); // OK 63 | takesString($key); // ERROR: Array key is not guaranteed int 64 | takesInt($key); // ERROR: Array key is not guaranteed string 65 | } 66 | 67 | /** @return array */ 68 | function providesKeyAndValue() 69 | { 70 | return [new Person()]; 71 | } 72 | 73 | 74 | $keyAndValueArray = providesKeyAndValue(); 75 | takesAnyArray($keyAndValueArray); // OK 76 | takesValueTypeOnlyArray($keyAndValueArray); // OK 77 | takesIntKeyAndValueArray($keyAndValueArray); // OK 78 | takesStringKeyAndValueArray($keyAndValueArray); // ERROR: Expects array given array 79 | 80 | foreach($keyAndValueArray as $key => $value) { 81 | takesPerson($value); // OK 82 | takesArrayKey($key); // OK 83 | takesString($key); // ERROR: expects string, int given 84 | takesInt($key); // OK 85 | } 86 | -------------------------------------------------------------------------------- /tests/docblock/class_string.php: -------------------------------------------------------------------------------- 1 | $className 14 | * @return T 15 | */ 16 | function build(string $className) { 17 | return new $className; 18 | } 19 | 20 | function takesPerson(Person $person): void {} 21 | 22 | 23 | $person = build(Person::class); // $person is an object of type Person 24 | takesPerson($person); // OK 25 | 26 | $dog = build(Dog::class); // $dog is an object of type Dog 27 | takesPerson($dog); // ERROR $dog is not of type Person 28 | 29 | -------------------------------------------------------------------------------- /tests/docblock/edgecase_class_string_templates.php: -------------------------------------------------------------------------------- 1 | $className 17 | * @return T 18 | */ 19 | function build(string $className): object { 20 | return new $className; 21 | } 22 | 23 | 24 | 25 | /* 26 | * Explicit version. Satisfies both PHPStan and Psalm. 27 | */ 28 | 29 | /** 30 | * @template T of object 31 | * @param class-string $className 32 | * @return T 33 | */ 34 | function build2(string $className): object { 35 | return new $className; 36 | } 37 | -------------------------------------------------------------------------------- /tests/docblock/extending_types.php: -------------------------------------------------------------------------------- 1 | */ 15 | class PersonRepository extends Repository 16 | { 17 | } 18 | 19 | $personRepository = new PersonRepository(); 20 | 21 | $personRepository->persist(new Person()); // OK 22 | $personRepository->persist(new Animal()); // ERROR. Expecting Person, got Animal 23 | -------------------------------------------------------------------------------- /tests/docblock/extending_types_with_restriction.php: -------------------------------------------------------------------------------- 1 | */ 16 | public function supports(): string; 17 | 18 | /** @param T $job */ 19 | public function process($job): void; 20 | } 21 | 22 | 23 | /** @implements JobProcessor */ 24 | class EmailSenderJobProcessor implements JobProcessor 25 | { 26 | public function supports(): string 27 | { 28 | return SendEmailJob::class; 29 | } 30 | 31 | public function process($job): void 32 | { 33 | takesSendEmailJob($job); // OK 34 | takesCreatePdfJob($job); // ERROR. Expecting CreatePdfJob got SendEmailJob 35 | } 36 | } 37 | 38 | $emailSenderJobProcessor = new EmailSenderJobProcessor(); 39 | $emailSenderJobProcessor->process(new SendEmailJob()); // OK 40 | $emailSenderJobProcessor->process(new CreatePdfJob()); // ERROR. Expected SendEmailJob got CreatePdfJob 41 | -------------------------------------------------------------------------------- /tests/docblock/extending_types_with_restriction_2.php: -------------------------------------------------------------------------------- 1 | */ 16 | public function supports(): string; 17 | 18 | /** @param T $job */ 19 | public function process($job): void; 20 | } 21 | 22 | 23 | /** @implements JobProcessor */ 24 | class PersonProcessor implements JobProcessor // ERROR. Person does not extend Job 25 | { 26 | public function supports(): string // OPTIONAL. Expecting class-string got string. Given issue is with the whole class it doesn't matter if SA picks this up or not. 27 | { 28 | return Person::class; 29 | } 30 | 31 | public function process($job): void 32 | { 33 | // Not really valid 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/docblock/generator_full.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | function foo(): \Generator 12 | { 13 | $bool = yield 1 => 'foo'; 14 | 15 | return new Person(); 16 | } 17 | 18 | function takesInt(int $value): void {} 19 | function takesString(string $value): void {} 20 | function takesPerson(Person $person): void {} 21 | 22 | 23 | 24 | foreach (foo() as $key => $value) { 25 | takesInt($key); // OK 26 | takesString($value); // OK 27 | foo()->send(true); // OK 28 | 29 | 30 | takesString($key); // ERROR, expects string, given int 31 | takesInt($value); // ERROR, expects int, given string 32 | foo()->send("string"); // ERROR, expects bool, given string 33 | } 34 | 35 | $person = foo()->getReturn(); 36 | 37 | takesPerson($person); // OK 38 | takesInt($person); // ERROR: Expects Person, got int. 39 | 40 | -------------------------------------------------------------------------------- /tests/docblock/generator_simple.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | function foo(): \Generator 9 | { 10 | yield 1 => 'foo'; 11 | } 12 | 13 | function takesInt(int $value): void 14 | { 15 | } 16 | 17 | function takesString(string $value): void 18 | { 19 | } 20 | 21 | 22 | foreach (foo() as $key => $value) { 23 | takesInt($key); // OK 24 | takesString($value); // OK 25 | 26 | takesString($key); // ERROR, expects string, given int 27 | takesInt($value); // ERROR, expects int, given string 28 | } 29 | 30 | -------------------------------------------------------------------------------- /tests/docblock/namespace.php: -------------------------------------------------------------------------------- 1 | */ 21 | $rooms = []; // OK - class is defined in same namespace 22 | 23 | /** @var array */ 24 | $students = []; // OK - included via use statement 25 | 26 | /** @var array */ 27 | $subjects = []; // OK - FQCN is used 28 | 29 | /** @var array */ 30 | $teachers = []; // ERROR: Unknown class 31 | } 32 | -------------------------------------------------------------------------------- /tests/docblock/restricting_templates.php: -------------------------------------------------------------------------------- 1 | $shapeProcessor */ 16 | $shapeProcessor = new ShapeProcessor(); // OK 17 | 18 | /** @var ShapeProcessor $squareProcessor */ 19 | $squareProcessor = new ShapeProcessor(); // OK 20 | 21 | /** @var ShapeProcessor $personProcessor */ 22 | $personProcessor = new ShapeProcessor(); // ERROR: Person not subtype of Shape 23 | -------------------------------------------------------------------------------- /tests/docblock/restricting_templates_3.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | /** @return T */ 20 | public function value() 21 | { 22 | return $this->value; 23 | } 24 | } 25 | 26 | 27 | function takesInt(int $int): void {} 28 | 29 | function takesString(string $string): void {} 30 | 31 | 32 | $ageValueHolder = new ValueHolder(21); 33 | 34 | takesInt($ageValueHolder->value()); // OK 35 | 36 | takesString($ageValueHolder->value()); // ERROR. Passing int to a function expecting string. 37 | 38 | -------------------------------------------------------------------------------- /tests/docblock/template_class_no_constructor.php: -------------------------------------------------------------------------------- 1 | values[] = $item; 17 | } 18 | 19 | /** @return T */ 20 | public function next() 21 | { 22 | $value = array_shift($this->values); 23 | if ($value === null) { 24 | throw new LogicException("Queue is empty"); 25 | } 26 | return $value; 27 | } 28 | } 29 | 30 | 31 | /** @var Queue $stringQueue */ 32 | $stringQueue = new Queue(); // OK 33 | 34 | $intQueue = new Queue(); // ERROR. Unknown type for Queue 35 | 36 | -------------------------------------------------------------------------------- /tests/docblock/template_class_no_constructor_invalid.php: -------------------------------------------------------------------------------- 1 | values[] = $item; 17 | } 18 | 19 | /** @return T */ 20 | public function next() 21 | { 22 | $value = array_shift($this->values); 23 | if ($value === null) { 24 | throw new LogicException("Queue is empty"); 25 | } 26 | return $value; 27 | } 28 | } 29 | 30 | 31 | /** 32 | * @param Queue $queue 33 | */ 34 | function takesIntQueue(Queue $queue): void {} 35 | 36 | /* 37 | * The type of Queue must be known at the time it is used. 38 | * It would not be reasonable to infer type of Queue based on the fact it is eventually passed to a function that 39 | * specifies the type of the Queue. 40 | */ 41 | $intQueue = new Queue(); // ERROR. Unknown type for Queue 42 | $intQueue->add(1); 43 | takesIntQueue($intQueue); // ERROR. Expects Queue, given Queue 44 | -------------------------------------------------------------------------------- /tests/docblock/template_class_param.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | /** @return T */ 20 | public function value() 21 | { 22 | return $this->value; 23 | } 24 | } 25 | 26 | 27 | function takesInt(int $int): void {} 28 | 29 | function takesString(string $string): void {} 30 | 31 | /** @param ValueHolder $valueHolder */ 32 | function takesIntValueHolder(ValueHolder $valueHolder): void 33 | { 34 | takesInt($valueHolder->value()); // OK 35 | takesString($valueHolder->value()); // ERROR. Passing int to a function expecting string. 36 | } 37 | 38 | takesIntValueHolder(new ValueHolder(20)); // OK 39 | takesIntValueHolder(new ValueHolder("hello")); // ERROR. Passing ValueHolder, expected ValueHolder 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/docblock/template_class_property.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | /** @return T */ 20 | public function value() 21 | { 22 | return $this->value; 23 | } 24 | } 25 | 26 | 27 | function takesInt(int $int): void {} 28 | 29 | function takesString(string $string): void {} 30 | 31 | 32 | 33 | class Entity { 34 | 35 | /** @var ValueHolder */ 36 | public ValueHolder $ageValueHolder; 37 | 38 | public function __construct() 39 | { 40 | $this->ageValueHolder = new ValueHolder(24); 41 | } 42 | } 43 | 44 | $entity = new Entity(); 45 | 46 | takesInt($entity->ageValueHolder->value()); // OK 47 | takesString($entity->ageValueHolder->value()); // ERROR. Passing int, expects string. 48 | 49 | 50 | $entity->ageValueHolder = new ValueHolder(33); // OK 51 | $entity->ageValueHolder = new ValueHolder("hello"); // ERROR. Assigning ValueHolder to property that should be ValueHolder 52 | 53 | -------------------------------------------------------------------------------- /tests/docblock/template_class_return.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | /** @return T */ 20 | public function value() 21 | { 22 | return $this->value; 23 | } 24 | } 25 | 26 | 27 | function takesInt(int $int): void {} 28 | 29 | function takesString(string $string): void {} 30 | 31 | /** @return ValueHolder */ 32 | function givesIntValueHolder(): ValueHolder 33 | { 34 | return new ValueHolder(23); 35 | } 36 | 37 | $ageValueHolder = givesIntValueHolder(); 38 | 39 | 40 | takesInt($ageValueHolder->value()); // OK 41 | 42 | takesString($ageValueHolder->value()); // ERROR. Passing int to a function expecting string. 43 | 44 | -------------------------------------------------------------------------------- /tests/docblock/template_function.php: -------------------------------------------------------------------------------- 1 | */ 12 | private $values = []; 13 | 14 | /** 15 | * @param K $key of array-key 16 | * @param V $value 17 | */ 18 | public function add($key, $value): void 19 | { 20 | $this->values[$key] = $value; 21 | } 22 | 23 | /** 24 | * @param K $key 25 | * @return V 26 | */ 27 | public function getValue($key) 28 | { 29 | if (array_key_exists($key, $this->values)) { 30 | return $this->values[$key]; 31 | } 32 | throw new InvalidArgumentException("Key not found"); 33 | } 34 | } 35 | 36 | class Person {} 37 | function takesString(string $value): void {} 38 | function takesPerson(Person $value): void {} 39 | 40 | 41 | /** @var Map $people */ 42 | $people = new Map(); // OK 43 | 44 | $people->add("Anna", new Person()); // OK 45 | $people->add(1, new Person()); // ERROR. Argument 1 expected string, got int 46 | 47 | 48 | $person = $people->getValue('Anna'); 49 | 50 | takesPerson($person); // OK 51 | takesString($person); // ERROR, Expects string, got Person 52 | --------------------------------------------------------------------------------