When using chained method calls returning $this, an error is raised for "UndefinedMethod".
Samples:
self; no phpdoc: https://psalm.dev/r/3d454e34c2self; AND @return $this phpdoc: https://psalm.dev/r/bda78a0789self; AND @return static phpdoc: https://psalm.dev/r/86e41520e7It also won't use return $this to infer the return type if no return type is provided.
tl;dr: It works if phpdoc is provided, but it won't use return $this to clarify.
I found these snippets:
https://psalm.dev/r/3d454e34c2
<?php
abstract class Foo {
public function do(): self
{
echo 'do' . PHP_EOL;
return $this;
}
}
class Bar extends Foo {
public function something(): self
{
echo 'something' . PHP_EOL;
return $this;
}
}
$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):
ERROR: UndefinedMethod - 21:17 - Method Foo::something does not exist
https://psalm.dev/r/23a3e1ee01
<?php
abstract class Foo {
public function do(): Foo
{
echo 'do' . PHP_EOL;
return $this;
}
}
class Bar extends Foo {
public function something(): Bar
{
echo 'something' . PHP_EOL;
return $this;
}
}
$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):
ERROR: UndefinedMethod - 21:17 - Method Foo::something does not exist
https://psalm.dev/r/bda78a0789
<?php
abstract class Foo {
/**
* @return $this
*/
public function do(): self
{
echo 'do' . PHP_EOL;
return $this;
}
}
class Bar extends Foo {
/**
* @return $this
*/
public function something(): self
{
echo 'something' . PHP_EOL;
return $this;
}
}
$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):
No issues!
https://psalm.dev/r/86e41520e7
<?php
abstract class Foo {
/**
* @return static
*/
public function do(): self
{
echo 'do' . PHP_EOL;
return $this;
}
}
class Bar extends Foo {
/**
* @return static
*/
public function something(): self
{
echo 'something' . PHP_EOL;
return $this;
}
}
$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):
No issues!
Can you elaborate on why you think it should work differently?
I'll do my best!
/** @return $this */ - or with static), so having to redundantly provide the PhpDoc from what could be inferred seems... inefficient? And it appears that it _can_ be understood.EDIT: In fairness, phpstan also doesn't infer it.
If Psalm inferred the return type as $this here: https://psalm.dev/r/392e337d4d it would have to consider this: https://psalm.dev/r/fbc0ad66a6 an LSP violation, and there's clearly none. Returning $this in C::ugh() is an implementation detail. Since this would introduce ambiguity I don't think this is feasible, nor, in fact, desirable.
Edit: fixed second snippet
I found these snippets:
https://psalm.dev/r/392e337d4d
<?php
class C {
public function ugh(): static {
return $this;
}
}
Psalm output (using commit c23406f):
No issues!
https://psalm.dev/r/fbc0ad66a6
<?php
class C {
public function ugh(): static {
return $this;
}
}
class D extends C {
public function ugh(): static {
return clone $this;
}
}
Psalm output (using commit c23406f):
ERROR: MethodSignatureMismatch - 10:5 - Method D::ugh with return type 'D' is different to return type 'C' of inherited method C::ugh
Yeah, Psalm has followed the rule that it infers types only inside functions, and I don't think that's worth violating. PHP 8 supports a static return type (which Psalm will support fully) to help with this particular use-case.
I must admit I hadn't considered PHP8's changes, which do solve the issue.
Thanks to both of you for your quick replies, it's much appreciated!
So as solution (until PHP8), in any of our code (not that we have much fluent code tbh) if that arises our best bet would be to use phpdoc with @return static.
If I may end with a quick question: any tip you could suggest that could be used in consumer code (ie when consuming an external package) to solve that?
....or PR into that external package to add the static @return annotation 🤷♂️
@tdtm you can use stub files https://psalm.dev/docs/running_psalm/plugins/authoring_plugins/#stub-files
Or send PR to package :)
Thanks @andrew-demb. I'll check with that packages' maintainers if they'll accept a PR for it.