Override PHP docblock/annotation of method without overloading (PhpStorm autocompletion) - php

This is a question about the autocompletion behavior in PhpStorm (and possibly other IDEs) in conjunction with PHP docblocks.
I have to groups of classes in my application. First there are individual classes for various products (CarProduct, FoodProduct etc.), all inheriting from BaseProduct, and the counterpart for individual contracts (CarContract, FoodContract etc.), all inheriting from BaseContract.
<?php
class BaseContract
{
/** #var BaseProduct */
private $product;
/**
* #return BaseProduct
*/
public function getProduct()
{
return $this->product;
}
}
Now I have an instance of CarContract, and I wanna get some CarProduct specific information:
<?php
/* PhpStorm thinks, this is BaseProduct */
$product = $carContract->getProduct();
/* hence, getSpeed() is not available for PhpStorm */
$product->getSpeed();
The autocompletion is not working as I like. There are two workarounds for this, but both are not nice:
Overload getProduct() in the subclass, just with updated #return docblocks
Add /** #var CarProduct $product */ everywhere, where I access the product of a CarContract
Is there a "usual" way to solve something like this, or are my workarounds the only solutions?

PhpStorm does not really allow/does not support doing something like: have the same named class defined elsewhere and just use it as a reference for overriding definitions of real class. You can do that .. but IDE will warn with "multiple definitions of the same class" and it may introduce some weird behaviour/unexpected warnings...
Here is a ticket that ask for such feature: https://youtrack.jetbrains.com/issue/WI-851 -- watch it (star/vote/comment) to get notified on any progress.
Your options are: you can provide correct type hint locally (to local variable) using #var -- you already know it and that's first that you would think of:
<?php
/** #var \CarProduct $product */
$product = $carContract->getProduct();
$product->getSpeed();
Another possible way: instead of overriding actual method .. you can try doing the same but with #method PHPDoc -- will work with your code:
<?php
/**
* My Car Product class
*
* #method \CarProduct getProduct() Bla-bla optional description
*/
class CarContract extends BaseContract ...

Related

Make PhpStorm favor #param instead of type declaration when using generics

In a post on the software engineering forum me and the community agreed that one of my architecture problems might be best solved using generics. Since in PHP generics are only supported via doc tags, my interface declarations always have two param types hinted: The #param tag and the PHP type declaration:
<?php
/**
* #template T of NodeInterface
*/
interface DirectedLinkInterface
{
/**
* #param T $startNode
*/
public function setStartNode(NodeInterface $startNode);
}
The problem is that whenever I use PHP's type declaration, PhpStorm ignores the more specific template variable.
Example:
I have a class that implements a specific version of the generic interface:
/**
* #template-implements DirectedLinkInterface<DrawActionNode>
*/
class DirectedImageProcessingLink implements DirectedLinkInterface, FollowLinkInterface
{
/**
* #param DrawActionNode $startNode
*/
public function setStartNode(NodeInterface $startNode): void
{
$this->startNode = $startNode;
}
}
But when I use this class incorrectly, PhpStorm doesn't find the error, UNLESS I remove the type declarations:
$node = new WhateverNodeInterface();
$link = new DirectedImageProcessingLink();
$link->setStartNode($node); //No error found by PhpStorm
The above code will only be considered as faulty when I remove the type declaration, which I don't really want since a missing type declaration looks kind of old school php hackish, even if the hinted type is more generic than the PHPDoc.
<?php
/**
* #template T of NodeInterface
*/
interface DirectedLinkInterface
{
/**
* #param T $startNode
*/
public function setStartNode(/*No type declaration here*/ $startNode);
//Removing the type declaration as done above makes PhpStorm use the actual template
//variable which, in the interface user's code, will be more specific and thus errors can be better found
}
Do you know any way to convince PhpStorm to use the #param tag even though there is a type declaration? Or do you think that type declarations aren't that necessary if you use #param tags in combination with generics?

Is it possible to annotate a generic container class in a docblock

Say I have a class Book which has a property on it called $chapters. In the implementation I'm working with, this property is represented by a generic helper class called Collection, which in turn contains an array of Chapter classes.
Example:
class Book {
/** #var Collection */
public $chapters;
}
class Collection {
private $items;
public function getItems(): array {
return $this->items;
}
}
class Chapter {}
Is it possible to annotate the $chapters property in Book so that my IDE knows that it is a Collection object, but that a call to that collection's getItems() method will return an array of Chapter instances?
Can I do this without creating a child class of Collection and annotating that?
EDIT: I don't think I was clear in my goal. I'm looking to type hint a class which is outside of the Book class and give guidance on what it's $items property would be — something like this (which I'm sure is invalid):
class Book {
/**
* #var Collection {
* #property Chapter[] $items
* }
*/
public $chapters;
}
For anyone who is still looking how to solve this issue, here is the solution based on Psalm's article "Templating".
Generics (templating)
If you you are working with variable data type inside a class (or even function), you can use what is called generic data types (in PHP this might be called templating). You can use generic data types for situations, when you need to write a piece of code, which doesn't really care of the type it uses, but it might be important for the client/user of the code. In other words: you want let the user of your code (Collection) specify the type inside your structure without modifying the structure directly. The best example (which is conveniently the anwswer itself) are collections.
Collections don't really need to know about what type of data they hold. But for user such as yourself this information is important. For this reason we can create generic data type inside your Collection and everytime someone wants to use the class they can specify through the generic type the type they want the Collection to hold.
/**
* #template T
*/
class Collection
{
/**
* #var T[]
*/
private $items;
/**
* #return T[]
*/
public function getItems(): array
{
return $this->items;
}
}
Here we defined our generic type called T, which can be specified by the user of Collection such as follows:
class Book
{
/**
* #var Collection<Chapter>
*/
public $chapters;
}
This way our IDEs (or static analyzers) will recognize the generic data type inside Collection as Chapter (i.e. the T type becomes Book).
Disclaimer
Although this is the way to write generic types in PHP (at least for now), in my experience many IDEs struggle to resolve the generic type. But luckily analyzers such as PHPStan knows how to work with generics.

How to use a $this reference inside PHP docblocks

I have a project that utilised MVC where the view file is inherting $this which refers to a view class attached to the controller.
Helper classes have been attached in some of the views and are used like follows:
<?=$this->someHelper->renderSomething()?>
I was hoping to help devs and the IDE out by doing this:
/** #var SomeHelper $this->someHelper */
It's not supported, seemingly. Is there a way to achieve this?
I can only find a workaround at the moment, to declare the helper as a new variable and include a #var statement for that.
It's not possible, you are supposed to type hint the $this instead. If $this is not any concrete class you can type hint, create a fake class/interface instead which will act as a helper to the IDE:
// somewhere outside of your code base, but accessible by the IDE
// use the name of your choice
interface CodeIgniterMvc
{
/**
* #return string
*/
function renderSomething(): string;
/**
* #param array $filter Filtering conditions
* #return \Your\App\Models\User[]
*/
function getUsers(array $filter): array;
}
and in the views:
/** #var $this CodeIgniterMvc **/
Of course include in the repository so every team member can gain such benefits.

Is it bad practice to inject several arguments to the constructor?

I'm developing a quite complex logistics management system which will keep growing into several other ERP related modules. Therefore, I am trying to have as much of the SRP and Open/Close Principles in place for ease of extension and domain based management.
Therefore, I decided to use Laravel and the following pattern (not sure if this has a name or not):
I will use the PRODUCT object for my example.
An object/entity/domain has a Class
class ProductService {}
This class has a Service Provider which is included in the providers array and is also autoloaded:
ProductServiceServiceProvider
The service provider instantiate (makes) the ProductRepository which is an interface.
The interface currently has a MySQL (and some Eloquent) called EloquentProductRepository implementation(s) and a ProductRepositoryServiceProvider binds the implementation which is also loaded and in the providers array.
Now a product has many different attributes and relationships with other domains and because the other domains (or entities) need to be fully detached and again abiding with the above principle (SRP etc..) I decided to also have the same structure for them as i do for the product...I know some might think that this is too much but we need to have the system very extendable and to be honest I like to be organised and have a uniform pattern (it doesn't take that much more time and saves me a lot later).
My question is this. The ProductService which handles all the business logic of the Product and makes the "Product" what it is will have several dependencies injected on creation of it's instance through the constructor.
This is what it has at the moment:
namespace Ecommerce\Services\Product;
use Ecommerce\Repositories\Product\ProductRepository;
use Ecommerce\Services\ShopEntity\ShopEntityDescriptionService;
use Content\Services\Entity\EntitySeoService;
use Content\Services\Entity\EntitySlugService;
use Ecommerce\Services\Tax\TaxService;
use Ecommerce\Services\Product\ProductAttributeService;
use Ecommerce\Services\Product\ProductCustomAttributeService;
use Ecommerce\Services\Product\ProductVolumeDiscountService;
use Ecommerce\Services\Product\ProductWeightAttributeService;
use Ecommerce\Services\Product\ProductDimensionAttributeService;
/**
* Class ProductService
* #package Ecommerce\Services\Product
*/
class ProductService {
/**
* #var ProductRepository
*/
protected $productRepo;
/**
* #var ShopEntityDescriptionService
*/
protected $entityDescription;
/**
* #var EntitySeoService
*/
protected $entitySeo;
/**
* #var EntitySlugService
*/
protected $entitySlug;
/**
* #var TaxService
*/
protected $tax;
/**
* #var ProductAttributeService
*/
protected $attribute;
/**
* #var ProductCustomAttributeService
*/
protected $customAttribute;
/**
* #var ProductVolumeDiscountService
*/
protected $volumeDiscount;
/**
* #var ProductDimensionAttributeService
*/
protected $dimension;
/**
* #var ProductWeightAttributeService
*/
protected $weight;
/**
* #var int
*/
protected $entityType = 3;
public function __construct(ProductRepository $productRepo, ShopEntityDescriptionService $entityDescription, EntitySeoService $entitySeo, EntitySlugService $entitySlug, TaxService $tax, ProductAttributeService $attribute, ProductCustomAttributeService $customAttribute, ProductVolumeDiscountService $volumeDiscount, ProductDimensionAttributeService $dimension, ProductWeightAttributeService $weight)
{
$this->productRepo = $productRepo;
$this->entityDescription = $entityDescription;
$this->entitySeo = $entitySeo;
$this->entitySlug = $entitySlug;
$this->tax = $tax;
$this->attribute = $attribute;
$this->customAttribute = $customAttribute;
$this->volumeDiscount = $volumeDiscount;
$this->dimension = $dimension;
$this->weight = $weight;
}
`
Is it bad practice to have as much arguments passed to the constructor in PHP (please ignore the long names of the services as these might change when the ERP namespaces have been decided upon)?
As answered by Ben below, in this case it is not. My question was not related to OOP but more to performance etc.. The reason being is that this particular class ProductService is what web deves would do with a controller, i.e. they would probably (and against principles) add all DB relationships in one ProductController which handles repository services (db etc..) and attaches relationships and then it suddenly becomes your business logic.
In my application (and I see most applications this way), the web layer is just another layer. MVC takes care of the web layer and sometimes other Apis too but I will not have any logic except related to views and JS frameworks in my MVC. All of this is in my software.
In conclusion: I know that this is a very SOLID design, the dependencies are injected and they really are dependencies (i.e. a product must have tax and a product does have weight etc..) and they can easily be swapped with other classes thanks to the interfaces and ServiceProviders. Now thanks to the answers, I also know that it is Okay to inject so many dependencies in constructor.
I will eventually write an article about the design patterns which I use and why I use them in different scenarios so follow me if you're interested in such.
Thanks everyone
Generally, no, It's not a bad practice, in most cases. But in your case, as said in the comments by #zerkms, it looks like your class is depending on a lot of dependencies, and you should look into it, and think on how to minimize the dependencies, but if you're actually using them and they should be there, I don't see a problem at all.
However, you should be using a Dependency Injection Container (DIC).
An dependency injection container, is basically a tool which creates the class by the namespace you provide, and it creates the instance including all the dependencies. You can also share objects, so it won't create a new instance of it while creating the dependencies.
I suggest you to ue Auryn DIC
Usage:
$provider = new Provider();
$class = $provider->make("My\\App\MyClass");
What happens here is this:
namespace My\App;
use Dependencies\DependencyOne,
Dependencies\DependencyTwo,
Dependencies\DependencyThree;
class MyClass {
public function __construct(DependencyOne $one, Dependency $two, DependencyThree $three) {
// .....
}
}
Basically, the Provider#make(namespace) creates an instance of the given namespace, and creates the needed instances of it's consturctor's parameters and all parameter's constructors parameters and so on.

Preserving auto-completion abilities with Symfony2 Dependency Injection

I'm using PHP Storm as my IDE, but I believe that other IDE's such as Netbeans will have the same issue as I'll explain below.
When using a framework like Symfony2, we have the wonderful world of Dependency Injection added. So objects can simply be instantiated using code like the following snippet:
$myThingy = $this->get('some_cool_service');
This is very handy, as objects are already configured beforehand. The one problem is, that auto-completion breaks entirely in basically any PHP IDE, as the IDE does not know what type the get() method is returning.
Is there a way to preserve auto-completion? Would creating for example an extension of Controller be the answer? For example:
class MyController extends Controller {
/**
* #return \MyNamespace\CoolService
*/
public getSomeCoolService() {
return new CoolService();
}
}
and then for application controllers, specify MyController as the base class instead of Controller?
What about using a Factory class, or any other possible methods?
It is more involving, but you can still do this with eclipse PDT:
$myThingy = $this->get('some_cool_service');
/* #var $myThingy \MyNamespace\CoolService */
UPDATE:
The example on this page shows you may also use the other way round with phpStorm:
$myThingy = $this->get('some_cool_service');
/* #var \MyNamespace\CoolService $myThingy */
You could define private properties in your controllers
class MyController extends Controller
{
/**
* #var \Namespace\To\SomeCoolService;
*/
private $my_service;
public function myAction()
{
$this->my_service = $this->get('some_cool_service');
/**
* enjoy your autocompletion :)
*/
}
}
I use base Controller class for bundle. You need to annotate the return in method. At least that works on Eclipse.
/**
* Gets SomeCoolService
*
* #return \Namespace\To\SomeCoolService
*/
protected function getSomeCoolService()
{
return $this->get('some_cool_service');
}
I don't like /*var ... */, because it gets too much into code.
I don't like private properties, because you can wrongly assume that services are already loaded.
I use Komodo Studio, and tagging variables with #var, even inside methods, preserves auto completion for me.
namespace MyProject\MyBundle\Controller;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\Request;
class WelcomeController extends ContainerAware
{
public function indexAction()
{
/*#var Request*/$request = $this->container->get('request');
$request->[autocomplete hint list appears here]
}
}
working with netbeans IDE 7.1.2 PHP

Categories