PHP 7.1 and PHPDocs

We start discussing PHPDocs in PHP 7.1 project in other topics:
https://discourse.zendframework.com/t/feedback-on-problem-details-module/107/13?u=webimpress
https://discourse.zendframework.com/t/component-upgrade-for-php-7-1/28/9
https://discourse.zendframework.com/t/how-should-we-provide-return-type-declarations-for-fluent-interfaces/30/17

it will be nice to discuss at here as separate issue.

Why to bother?

With PHP 7.1 we can use type hinting (also for simple types) and return types defined next to the function. Often PHPDoc comment looks like duplicated information we already provided in method signature. However in PHPDocs we can add much more information and be more specific (for example as @PowerKiKi said: type-hint array and PHPDocs int[]). In PHPDocs we have to always add @throws tags as it is not specified anywhere in method signature (probably we should include all throws, even if it is thrown from other method call inside the function).

What do you think?

Should we always add FULL PHPDocs with @params, @return, @throws… even if it duplicates some information or add only needed PHPDocs part (when something is “different” than in signature)?

My opinion is that we should keep full PHPDocs, because:

  • it will be consistent and clear
  • we can use automated tools to sync comments with method signature (phpcs?)
  • we can specify more information in PHPDocs, always have everything there, all params, throws and return type
  • if we need add description to the variable/return type/throws we add it in PHPDocs. Not always is possible to chose descriptive-enough and short variable/param name.

If we decide to add only PHPDocs tags only when needed I can see following issues:

  • if we want to add description to one param should we define all others also in PHPDocs?
  • if we want to change type of variable (to be more specific - int[]) should we add also all others?
  • how IDE/phpdocumentor (if anybody is using it) is gonna work with partial informations in PHPDocs?
  • how we make sure that PHPDocs added only when needed is right and it’s not a bug - if we define it only in some functions it would be not clear if it is right that it’s there or its just mistake/residue.
  • if we are going to use libraries that supports PHP 5.6/PHP 7.0 there will not be type-hinting/return types. If we implement/extend some interface/class from that library we would be able to add type hinting/return type because of declaration incompatibility. Should we then add only @inheritDoc, nothing, or PHPDocs with params/return/throws tags?

What do you think? What do you prefer? Maybe you have another ideas/comments?

I personally prefer to have docblocks only if they provide more information than I can glean from the signature. This would mean:

  • Document with @param annotations only if the type varies, or if explanation of its purpose and/or value are required. This needs only be done for the specific arguments where this is needed, as the annotation includes the argument name.
  • Document with @return annotation only if the type varies, or the return value needs an explanation.
  • Document with @throws ALWAYS when a method explicitly throws one or more exceptions.
  • If you can provide a meaningful description detailing the method’s purpose and usage.
  • Document with @inheritDoc if the parent method’s docblock has additional information.

To answer the last bullet point, we will only be supporting PHP 7.1+ going forward, so the above should largely answer those things.

All of that said: my opinion is largely contingent on how phpDocumentor and IDEs interpret docblocks in these situations. If they do not handle them cleanly, then we should likely be consistent and add them everywhere, and, in such a case, I’d support having a phpcs rule that would detect if they are out of sync (and, better yet, fix them!).

Thats how PhpStorm 2017.1.4 (May 16, 2017) generates docblocks:

    /**
     * @param string $key
     * @param \ArrayObject $container
     * @return string
     * @throws \Exception
     */
    public function methodExample(string $key, \ArrayObject $container): string
    {
        if ($container->count() === 0) {
            throw new \Exception();
        }
        if (!$container->offsetExists($key)) {
            throw new \RuntimeException();
        }
        if (!is_string($value = $container->offsetGet($key))) {
            throw new \LogicException();
        }
        return $value;
    }

By default parameters and return type is added to docblock. Mismatch between docblock and method signature is reported as invalid docblock (warning, part of docblock underlined). Runtime and logic exceptions are skipped (default code style).

Personally I like when docblocks are always there, even if something is self-explanatory. That’s also what I would expect from framework like Zend, where we got that “configuration over magic” (I know, it’s not related, but still got that on my mind).

@cpm Question: what does PHPStorm display for typehints for a method you’re calling if that method has PHP 7.1+ typehints and a subset of @param annotations?

The behaviour is consistent whether the docblock is there or not: the IDE
will consider both definitions like with union types if there is a mismatch.

It will also report un-documented @throws if any callee has an unhanded
thrown exception that is documents as @throws in the callee.

If it was for me, and we had full PHP 7.1 hints, I would completely drop
docblocks except for following scenarios:

  • @param array (this always requires better specifications)
  • Same for iterable/Traversable
  • @throws of any sort: if the method throws or rethrows. This may look
    annoying, but is extremely important to eagerly detect BC breaks related to
    thrown exception types. A glimpse at the docblock changes in a patch and
    you know that an added or removed @throws is a problem

Exactly like that, it creates union if there is mismatch between both definitions (so type hint is like “string|int”)

All about PHPStorm is true, I’d like to highlight some issues:

  • by default PHPStorm generates full PHPDocs (always when we type before method /** and press “enter” the whole PHPDocs is generated) - does anyone know if we can configure it? I couldn’t find that option.
  • by default PHPStorm highlights issues if PHPDocs is not complete/not consistent - when there is missing param declaration, return type, throws or inconsistent types. I just found that it is configurable and it could be disabled to not check PHPDocs at all.

Is anyone using PHPStorm EAP? Are there any changes in case of PHPDoc and PHP7.1?

I’ve tested phpDocumentor with full and partial PHPDocs on PHP7.1. Here is repository I’ve created https://github.com/webimpress/php-docs and here is generated documentation for the library:
https://webimpress.github.io/php-docs/index.html
I described everything there, to summarize: phpDocumentor does not support PHP 7.1 yet, and because of that there are some issues. Mainly this tool is generating documentation based on PHPDocs, but it also analyze code in some degree (method params). Maybe with PHP 7.1 support it will be changed/improved. Last release (2.9.0) been on the 22nd of May 2016… :frowning:

I was thinking about partial docs and still I’m not convinced, but now I thinking that it should be possible to write also some sniffs which checks these PHPDocs and say if these are correct or not. We just need to specify all rules, when we can specify annotations and should be fine. But my biggest worry is that with partial documentation we have to look in “two places” to have full picture what is the function, what parameters it accepts and what exceptions it throws. We wouldn’t be able to read all of it from PHPDocs but from PHPDocs and function declaration. So when we look on the function we will create the same in our head what PHPStorm does with suggestion - union between PHPDocs annotations and function declaration. Probably it’s fine in case of small methods, not sure if in all cases it will be good enough.

Regarding @throws annotation, IMHO we should always define all @throws - doesn’t matter if function throws it explicit or implicit, I mean:

class Test
{
  /**
   * @throws ErrorException
   * @throws RuntimeException
   */
  public function m1(?int $a) : int
  {
    if ($a) {
      throw new ErrorException();
    }

    return $this->m2();
  }

  /**
   * @throws RuntimeException
   */
  public function m2() : int
  {
    throw new RuntimeException();
  }
}

@ocramius is that what you meant?

Another question: how we should specify ‘better’ type for iterable/Traversable? Should it be iterable<string> or string[]? If we go with first option, then for arrays we should do array<string> or string[], or maybe even array<int, string> which means “array with integer keys and string values”?
Currently PHPStorm for int[] in suggestion shows type as \int[], for array<int, int> suggestion is just array.

I think https://www.thecodingmachine.com/type-hint-all-the-things/
clarifies this

Marco Pivetta

http://twitter.com/Ocramius

http://ocramius.github.com/

1 Like

That’s not that easy. You have to consider that, as soon as you have any kind of dependency you are using, your method can suddenly throw anything (if you don’t have direct control over that dependency).

Yesterday I’ve pushed new sniffs to ZendCodingStandard library with sniffs to check params, return type and throws in function. I’ve started preparing PR, first for zend-expressive-hal to check how it works with these sniffs and I have found several issues:

  1. mixed/mixed[] type is sometimes useful, especially in tests, when we have for example:
public function invalidAttributeNames()
{
    return [
        'null'         => [null],
        'false'        => [false],
        'true'         => [true],
        'zero'         => [0],
        'int'          => [1],
        'zero-float'   => [0.0],
        'float'        => [1.1],
        'empty-string' => [''],
        'array'        => [['attribute']],
        'object'       => [(object) ['name' => 'attribute']],
    ];
}

I think then we should allow using it.

  1. What type we should define for multidimensional arrays, like:
return [
    'foo' => 'bar',
    'id'  => 12345678,
    '_links' => [
        'self' => [
            'href' => '/api/foo',
        ],
        'about' => [
            ['href' => '/doc/about'],
            ['href' => '/doc/resources/foo'],
        ],
    ],
    '_embedded' => [
        'bar' => [
            'bar' => 'baz',
            '_links' => [
                'self' => ['href' => '/api/bar'],
            ],
        ],
        'baz' => [
            [
                'baz' => 'bat',
                'id'  => 987654,
                '_links' => [
                    'self' => ['href' => '/api/baz/987654'],
                ],
            ],
            [
                'baz' => 'bat',
                'id'  => 987653,
                '_links' => [
                    'self' => ['href' => '/api/baz/987653'],
                ],
            ],
        ],
    ],
];

In that case mixed[] can be a bit confusing (as it is not one-level array). Any ideas? Maybe we should allow just array?

  1. It is not always possible to specify type for array/iterable/\Traversable, for example:
    https://github.com/zendframework/zend-hydrator/blob/master/src/ExtractionInterface.php#L18

Another example would be from zend-expressive-hal:
https://github.com/zendframework/zend-expressive-hal/blob/master/src/ResourceGenerator/ExtractCollection.php#L44

These methods are very generic and we can’t provide better specifications.
I think we shouldn’t force it. Of course it should be allowed to provide better specification, because in some cases it will be possible and useful.

  1. Type Generator - functions with yield, for example:
    https://github.com/zendframework/zend-expressive-hal/blob/master/test/Metadata/MetadataMapFactoryTest.php#L105-L117
    It’s kinda similar to Traversable, in perfect world we would like to have better specification but I think it is not always possible or pretty hard to do.

  2. Tag @inheritDoc and {@inheritDoc}
    https://github.com/phpDocumentor/fig-standards/blob/master/proposed/phpdoc.md#61-making-inheritance-explicit-using-the-inheritdoc-tag
    The difference is:

  • @inheritDoc when we inherit everything from parent method and we are not providing anything extra,
  • {@inheritDoc} - when we inherit everything from parent and we extend description or specify type, more throws etc.
    What do you think, should we allow using both of them? Currently I do not support any of them, and require define params/return type/throws always (only when needed as we agreed), but I can imagine in some cases with inheritance it will be duplicated many times.
  1. Arrays with non-integer keys - let say array of strings with strings keys.
    Here: http://www.icosaedro.it/phplint/phplint2/doc/reference.htm?p=docblocks#literalarrays
    is suggested to use string[string] (old notation was array[string]string). If we go with string[string] PHPStorm doesn’t support it at all - and thinks that type is string not even string[].
    Of course if keys are integers we should omit int in square brackets (as it is default).
    Another notation I’ve seen is: array(string => string) (also not supported by PHPStorm).

  2. Arrays with mixed values: but only (let say) int and string:
    https://github.com/phpDocumentor/fig-standards/blob/master/proposed/phpdoc.md#arrays
    It’s suggested there to use (int|string)[]. As I know, many people used int[]|string[] in that case, but I think (int|string)[] is more accurate - it means (for me) “array which contains ints and strings (at the same time)”, when int[]|string[] means “array of ints XOR array of strings”.

  3. Generic types:
    https://github.com/phpDocumentor/fig-standards/blob/master/proposed/phpdoc.md#collections
    For example:

/**
 * @param \ArrayObject<string>
 */

Should we allow that style of definition?

  1. Defining array values:
    https://github.com/phpDocumentor/fig-standards/blob/master/proposed/phpdoc.md#examples-12
/**
 * @param array $options {
 *     @var bool   $required Whether this element is required
 *     @var string $label    The display name for this element
 * }
 */

We do something similar here:

It is not supported by PHPStorm as far as I know.
Should we implement it or not?

That’s it for now. Please let me know what do you think on each of above points.
Below I prepared short questionnaire for all above points, hope it will help :smiley:


1. Allow mixed / mixed[] types

  • yes
  • no
  • another suggestion

0 voters

2. Multidimensional arrays:

  • mixed[]
  • just array
  • another suggestion

0 voters

3. Do not require better specification for following types:

  • array
  • iterable
  • \Travesable
  • \Generator
  • always require better specification for above types
  • another suggestion

0 voters

4. Generators (included in point 4)

  • any suggestions?

5. @inheritDoc and {@inheritDoc}

  • disallow using both, always params/return type/throws need to be provided
  • allow using both (CS checks cannot be done on method when one of these tags is provided)
  • another suggestion

0 voters

6. Array with non-integers keys (array of strings with string keys):

  • string[] is enough
  • use string[string]
  • use array[string]string
  • use array(string => string)
  • another suggestion

0 voters

7. Array with mixed values (int and string only):

  • allow (int|string)[]
  • int[]|string[] is clear enough and use it
  • another suggestion

0 voters

8. Generic types:

  • allow \ArrayObject<key-type, value-type>
  • it is useless
  • another suggestion

0 voters

9. Defining array values:

  • use PSR-5 proposed standard with @var for each key
  • keep old ‘standard’ (as it is in zend-code) with @configkey tag
  • we don’t need any standards :smiling_imp:
  • another suggestion

0 voters

A few quick notes/observations:

  • I see 3 and 4 as essentially the same question, particularly with the way you wrote the poll for 3. Generator is a valid return type, and while it may not indicate the items available, the main point is that you’re getting an iterable set. If more information is needed, that’s what the docblock is for. When a more specific type is known or expected, rule 8 surrounding collections/generics can kick in.

  • 6 and 9 are also related. I’d argue that a mixed[] or string[] or similar indicates that an array is being returned with values of the specified type(s), while the { @var ... } notation indicates that an associative array is indicated.