DocBlock class type inheritance - php

Although this question is about DocBlocks in general, my use-case is about PHP.
Consider the following PHP code:
<?php
class ParentClass {
/**
* Says 'hi' to the world.
* #return ParentClass Returns itself for chaining.
*/
public function say_hi(){
echo 'hi';
return $this;
}
}
class ChildClass extends ParentClass {
/**
* Says 'bye' to the world.
* #return ChildClass Returns itself for chaining.
*/
public function say_bye(){
echo 'bye';
return $this;
}
}
$c = new ChildClass;
$c->say_hi()->say_b| <- type hinting won't suggest "say_bye" here
?>
This is just a trivial class with some chaining. The extended class looses type hinting because the parent class' docblock is making use of a specific class name which does not have the methods/properties of the child class.
Assuming we do want type-hinting capability, (if not, please leave this question - I don't want useless arguments here), how should I fix this?
I came up with the following possibilities:
Change the PHPDoc standard to allow a special keyword
Add a superfluous say_hi() method which calls the parent just to redeclare docblock
Do not specify the return type at all, let the IDE decide what return $this; means (does this even work?)

You can solve this like this :
class ParentClass {
/**
* Says 'hi' to the world.
* #return static
*/
public function say_hi(){
echo 'hi';
return $this;
}
}
The "#return static" statement allows exactly what you want, PhpStorm works just fine with it.

What you describe is commonly called a "fluent interface", where all of an object's methods do their work and return the object itself.
I have not personally seen any PHPDoc guideline finalized on how to do exactly that. As such, I'm not aware that any IDE has put forth a means for its autocomplete functionality to handle the use case.
A likely path that PHPDoc will take on this is to utilize "#return $this" to be the convention to indicate fluent methods, as it matches the code syntax itself and is therefore very clear. I doubt that any IDEs will build that capability in until the standard itself incorporates this use case.
In the short term, I think that your superfluous "ChildClass::say_hi(){parent::say_hit();}" might get your IDE autocompletion to work. Again, might, because having the autocomplete also recognize method chaining itself (e.g. $foo->bar()->baz()->roll()->tide();) might not exist.

Related

Documenting abstract factory method return types in PHP with docblocks

This has been asked again and again, but the replies are a bit old and I'm somewhat desperately hoping something changed since "can't be done" replies.
Context:
class AbstractBuildObject {}
class Hammer extends AbstractBuildObject{}
class Nail extends AbstractBuildObject{}
class AbstractFactory{
/**
* return $type
*/
public function build1(string $type): AbstractBuiltObject {
return new $type();
}
/**
* return any(AbstractBuiltObject)
*/
public function build2(string $someArg): AbstractBuiltObject {
$type = $this->decideTypeBasedOnArgsAndState($someArg);
return new $type();
}
}
I tried to represent what I need with the annotations above the builders.
return $type (or ideally return $type of AbstractBuiltObject should hint that the return type is specified in the input parameter.
In the second case, any(AbstractBuiltObject) signifies that any derived concretion of the abstract class might be returned.
So I need some kind of annotation to achieve the effects I described. These annotations obviously don't work, I just used them for illustrating the concept.
I know one might be tempted to use pipe type joins like return Hammer|Nail, but in my case, the factory class should hold be modified every time a new concrete implementation is added to the project, it's also not specific enough in the build1 case, where I know precisely what the return type should be.
So, in short, I need this to work at least in PhpStorm:
(new AbstractFactory())->build1(Hammer::class)-> // I should have Hammer autocomplete here
(new AbstractFactory())->build2('foo')-> // I should have autocomplete of any concretion of the abstract here
Philosophical conversations about breaking the D in SOLID aside, if you want something like this to autocomplete the methods that are available only on Hammer:
(new AbstractFactory())->build1(Hammer::class)->
Then you have already committed to writing this block of code specifically for the Hammer class. And if you're going to do that, then you might as well do this:
$hammer = (new AbstractFactory())->build1(Hammer::class);
And if you do that, then you might as well do this:
/**
* #var Hammer
*/
$hammer = (new AbstractFactory())->build1(Hammer::class);
And then your autocomplete on $hammer-> should work.
The solution we adopted is this:
<?php
class AbstractBuiltClass {
/**
* #return static
*/
public static function type(self $instance)
// change return type to :static when #PHP72 support is dropped and remove explicit typecheck
:self
{
if(!($instance instanceof static)){
throw new Exception();
}
return $instance;
}
}
class Hammer extends AbstractBuiltClass{
function hammerMethod(){}
}
Hammer::type($factory->get(Hammer::class))->hammerMethod();
Contenders for a viable solution:
Psalm template annotations: https://psalm.dev/docs/annotating_code/templated_annotations/ very promising but not widely supported yet
Variable docblock (see Alex Howansky's answer)

Correct phpDoc to tell IDE of dynamic class return type

Consider this code:
class ParentClass {
/** #return ParentClass */
public function getNew(){
return call_user_func(array($this,'generator'));
}
protected function generator(){
return new static(); // will actually return whatever class calls this code
}
}
class ChildClass extends ParentClass {}
$child = new ChildClass();
var_dump($child->getnew()); // object(ChildClass)
Because ParentClass::generator() returns a static(), when the child instance calls getNew(), a ChildClass() is returned. The IDE (PhpStorm in my case) has no way to resolve this since the generator is dynamically called with call_user_func(). As a result, the IDE thinks that a ParentClass instance will be returned:
Is there a way to improve the parent's phpDoc block to better reflect the return type?
I figured it out, using this phpDoc block for ParentClass::getNew():
/** #return static */
public function getNew(){/*...*/}
phpDoc Types keywords
self - An object of the class where this type was used, if inherited it will still represent the class where it was originally defined.
static - An object of the class where this value was consumed, if inherited it will represent the child class. (see late static binding in the PHP manual).
$this - This exact object instance, usually used to denote a fluent interface.
PhpStorm 2017.2 understands it, and recognizes that $child->getNew() will return a child instance. The IDE doesn't quite call it ChildClass, but rather static. Still it autocompletes the methods and properties that belong exclusively to the child class.

Is there a way to indicate that a class has magic methods defined for every method on another class?

Is there a way to document that a certain class has magic methods for every method defined in another class?
I am using PhpStorm, so I would be happy with any solution that will get autocomplete to work properly for that.
class A
{
// a bunch of functions go here...
}
/**
* Class B
* What should go here to make it work???
*/
class B
{
private $aInstance;
public function __construct() {
$this->aInstance = new A();
}
public function __call($name, $arguments) {
// TODO: Implement __call() method.
if(method_exists($this->aInstance, $name)) {
return $this->aInstance->{$name}(...$arguments);
}
throw new BadMethodCallException();
}
// a bunch more functions go here...
}
The proper solution is to use supported #method PHPDoc tags. This way it will also work in other editors/IDEs that support PHPDoc and understand such standard tag.
This approach requires every method to be listed separately. More on this in another StackOverflow question/answer: https://stackoverflow.com/a/15634488/783119.
In current PhpStorm versions you may use not-in-PHPDoc-specs (and therefore possibly PhpStorm-specific) #mixin tag.
Adding #mixing className in PHPDoc comment for your target class should do the job for you.
/**
* Class B
*
* #mixin A
*/
class B
{
Basically, #mixin tag does what actual PHP's traits do.
Please note that there is no guarantee that support for such tag will not be removed at some point in the future, although it's pretty unlikely.

How to tell phpDoc a string is a class name?

I often give objects static methods and properties that do not require the object to be initialized. For example:
class SomeObject {
public function __construct($object_id) {
$this->loadProperties($object_id);
}
public static function getSomeStaticString() {
return "some static string";
}
}
Now we subclass these objects and have some sort of controller that returns an object class string under certain circumstances where the object should not yet be initialized. For example:
class SomeObjectController {
public function getSomeObjectWithTheseProperties(array $properties) {
if($properties[0] === "somevalue") {
if($properties[1] === "someothervalue") {
return SomeSubclassObject::class;
}
return SomeObject::class;
}
return NULL;
}
}
At times I might want to call the static function SomeObject::getSomeStaticString() without actually initializing the object (because that would involve an unneeded database fetch). For instance:
$controller = new SomeObjectController;
$properties = array("somevalue", "someothervalue");
$object_class = $controller->getSomeObjectWithTheseProperties($properties);
echo $object_class::getSomeStaticString();
Question: can I somehow tell PhpStorm, preferably through phpDoc, that $object_class is a class string of a subclass of SomeObject?
If I tell my IDE it's a string, it will notify me getSomeStaticString() is an invalid method. On the other hand, if I tell my IDE it's an instance of SomeObject, it thinks I can access regular non-static methods and properties, which I can't.
PHPStan uses the class-string PHPDoc type for this. It has been supported by PHPStorm since 2020.3, but it seems to work properly (with all autocompletion available) as of 2021.2, when they added support for Generics.
In your case the return type annotation would be #return class-string<SomeObject>.
/** #var SomeObject $object_class */
$object_class = $controller->getSomeObjectWithTheseProperties($properties);
Sorry, no other way as to tell that it's an instance of SomeObject.
... if I tell my IDE it's an instance of SomeObject, it thinks I can access regular non-static methods and properties, which I can't.
So? Just do not access non-static methods and properties.
UPDATE 2021-10-26: Since v2020.3 PhpStorm has Psalm Support and PHPStan Support plugins bundled. They support most of the syntax/types supported by those tools (you can check PhpStorm Issue Tracker here for all the tickets for those plugins (resolved and still pending)).
With those tools you can use class-string pseudo type:
https://psalm.dev/docs/annotating_code/type_syntax/scalar_types/#class-string-interface-string
https://phpstan.org/writing-php-code/phpdoc-types#class-string
If you want perfect type hinting you may create an Interface which only lists the static methods (and properties) of your class, and use that Interface as the type hint for your classname string returned from SomeObjectController::getSomeObjectWithTheseProperties().
It adds overhead to the maintenance to ensure the Interface and the classes are kept in sync, but if you need those type hints to work properly, this is the way.
You can also use this workaround:
/**
* #return string|SomeObject
*/
public function getSomeObjectClass()
{
return SomeObject::class;
}

PHPDoc and __callStatic

tl;dr
What is the correct way to annotate (in PHPDoc) functions implemented via __callStatic? More important: is there a way that will make NetBeans and PHPStorm understand that these are static methods?
Motivation
If you want the bigger picture, here's how I got to this question.
Problem: In my current project we have a ton of classes that should really be singletons (DB proxies and the like). Needless to say, we have at least a few hundred require_once and $foo = new FooProxy(); lines.
Solution: I created a Loader class to solve this, using the __callStatic magic method so we can just say $foo = Loader::FooProxy();. It's perfect for our purposes, but:
Problem: This way there's obviously no type hinting in either IDE used in the team.
Solution: Every module defines a subclass of Loader, adding methods that just route to __callStatic.
Problem: Adding actually interpreted code just for auto-completion's sake is not acceptable (this could be argued, but let's accept it for the time being).
Solution: Let's not add any real methods, only declare the methods in PHPDoc like this:
<?php
/**
* #method FooProxy FooProxy()
*/
class BarLoader extends Loader {}
?>
Problem: FooProxy is not a static method. None of the following make it static either:
<?php
/**
* #static
* #method FooProxy FooProxy()
*/
///////////////
/**
* #static #method A A()
* #method static A A()
* #method A static A()
* #method A A() static
*/
Making the class abstract makes no difference. About an hour of Google'ing turned up no solutions. The primary goal is to make the IDEs aware of these functions; having correct PHPDoc is not really a necessity.
Well, PhpStorm 3.0 will accept
#method static type name() description
See relevant feature request http://youtrack.jetbrains.net/issue/WI-4051
Generally speaking, I think the choice of using the magic stuff comes with caveat of having to accept a tradeoff of losing the effectiveness of things like autocompletion.
However, in my testing with Eclipse PDT (Helios with PHP 5.3.2 on WinXP), I was able to get good autocompletions from one explicit static method and two magic static methods out of my Loader class that I modeled after your example.
In short, it appears the use of the #method tag in the class docblock was enough for Eclipse to figure things out. If NetBeans and PHPStorm are having trouble, I'm not sure if it's related to the "static" aspect or not... it may just be that the parsing of such dynamic code may be more than their autocompletion logic is built to handle.
<?php
/**
* #method BarProxy BarProxy() returns an instance of BarProxy
* #method BazProxy BazProxy() returns an instance of BazProxy
*/
class Loader
{
public static function __callStatic($name, $arguments)
{
return new $name($arguments);
}
/**
* #return FooProxy
*/
public static function FooProxy(){
return new FooProxy();
}
}
class FooProxy
{
public function sayCheese() {}
}
class BarProxy
{
public function eatFries() {}
}
class BazProxy
{
public function sleep() {}
}
$foo = Loader::FooProxy();
$foo->sayCheese(); // did this simply to verify explicit autocompletion succeeded
$bar = Loader::BarProxy();
$bar->eatFries(); // autocompletion of just "$bar->" brought up "eatFries()"
$baz = Loader::BazProxy();
$baz->sleep(); // autocompletion of just "$baz->" brought up "sleep()"

Categories