PSR-5: declare @method and @property on directly to methods

1,196 views
Skip to first unread message

David Rodrigues

unread,
Mar 29, 2017, 12:48:10 PM3/29/17
to PHP Framework Interoperability Group
There some cases where it turn need. For instance, on Eloquent (from Laravel) we have a feature called "scope", that is a method prefixed by this term that is called by it radical, instead. For instance:

public function scopeFilterByName($name) { ... }

You should call this method like $query->filterByName($name) instead of $query->scopeFilterByName($name).

Currently we could declare it directly on class, like:

/** @method void filterByName($name) */
class MyClass extends Model { ... }

But is hard to IDE, for instance, understand that it is related to scopeFilterByName().

Then my suggestion is make this declaration directly on method that supports that:

/** @method filterByName($name) */
public function scopeFilterByName($name) { ... }

The advantages:

  1. IDE could detects exactly "who" is the real method, then read all parameters related, description, etc.;
  2. The @method return typehint is optional, when not declared, copy from real method, if declared, override that;
  3. The @method parameters is optional, when not declared, copy from real method, if declared, override that;
---

The same case could be applied to properties.

Again, on Eloquent, we have a feature called relations, that is declared as methods, but called as properties.

/** @property User[] $users */
class MyClass extends Model {
    ...

    /** @return HasMany */
    public function users() { ... }
}

Currently that is the way that we should declares it, but again, IDE can't understand it to identify that this property is a relation declared by users() method.

Then it should be the solution:

class MyClass extends Model {
    ...

    /**
          * @property User[] $users
     * @return HasMany
          */
    public function users() { ... }
}

The advantages are basically the same that @method.


Alexander Makarov

unread,
Apr 1, 2017, 1:40:29 PM4/1/17
to PHP Framework Interoperability Group
That looks like a very specific black magic. Not sure it is a good idea to provide such ability.
Message has been deleted

David Rodrigues

unread,
Apr 2, 2017, 4:54:19 PM4/2/17
to PHP Framework Interoperability Group
It's how the major part of frameworks are working now, by using features like magic methods from PHP to create some shortcuts on development. 

Sam Minnée

unread,
Apr 2, 2017, 10:05:15 PM4/2/17
to PHP Framework Interoperability Group
Applying the @method and @property items on a class basis can deal with magic methods.

By making it a per-method property it raises a whole bunch of other questions, like

 - Will there be a single method that clearly owns the given magic method?
 - Will the arguments of the magic method be the same as the underlying method, or will there be extra/removed arguments?
 - Will the return type of the magic method be the same as the underlying method?

In general it seems like a piece of functionality with only limited benefit over class-based @method and @property items, for a very specific use-case.

David Rodrigues

unread,
Apr 3, 2017, 7:49:52 AM4/3/17
to PHP Framework Interoperability Group
Ok, lets supposes that we are working with Eloquent, where it can be very helpful. 
I think about some possibilities that should be better.

The scope-method on Eloquent is described as "scope" + scope name, the "scopeFilterName()", for instance, is called only as "filterName()".
The major question here is that the scope-method will receives a first parameter that the filterName() will not.

While the scopeFilterName() signature is ($builder, [... $otherArguments]),
The filterName() signature is only ([... $otherArguments]).

Then I suggests you describe it like that:

/**
 * @param Builder   $builder    The scope builder instance.
 * @param string    $name       The name to filter.
 * @param bool|null $strictness The strictness of filter. 
 * @method filterName($name, $strictly = null)
 */
public function scopeFilterName(Builder $builder, $name, $strictness = null) { ... }

In my mind, it could works like a method aliases (that is partially the case).

The IDE, for instance, will understand that on filterName() declares only two parameters that was described in the generator-method.

It should works basically like I do directly:

/**
 * @param string    $name       The name to filter.
 * @param bool|null $strictness The strictness of filter. 
 */
public function filterName($name, $strictness = null) { ... }

Now replying your questions:

  • Applying the @method and @property items on a class basis can deal with magic methods.
Yes, it'll do, but the problem is that the IDE can't understand that, or even a documentor.
It'll understand as a magic method, but will not identify where it was declared.

  • Will the arguments of the magic method be the same as the underlying method, or will there be extra/removed arguments?
As I mentined before, it can do it easily.
Maybe there some case where it can receives more arguments, then you should declares it as part of the magic-method.

/**
 * The lessArguments() is called from moreArguments() magically.
 * @param string $name This parameter is used only on moreArguments().
 * @method moreArguments($name)
 */
public function lessArguments() { ... }

  • Will the return type of the magic method be the same as the underlying method?
You can declare it on annotation (@method (?returnType) magicCall((... arguments)) (?extra-description)).

I think too about an alternative, that can handle that directly on class, as today.

Basically, you can annotate the magic-owner of method, like that:

/**
 * @method magicChild() {magicParent}
 */
class MyClass {
    /** Description. */
    public function magicParent() { ... }
}

It can be useful if you need declares that outside the class:

/**
 * @method magicChild() {OtherClass::magicParent}
 */ 

Michael Mayer

unread,
Apr 3, 2017, 10:44:01 AM4/3/17
to PHP Framework Interoperability Group

On Wednesday, March 29, 2017 at 6:48:10 PM UTC+2, David Rodrigues wrote:
The advantages:
  1. IDE could detects exactly "who" is the real method, then read all parameters related, description, etc.;
  2. The @method return typehint is optional, when not declared, copy from real method, if declared, override that;
  3. The @method parameters is optional, when not declared, copy from real method, if declared, override that;
Mhm, lets consider a more suitable example from the eloquent docs

class User extends Model
{
   
/**
     * Scope a query to only include users of a given type.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param mixed $type
     * @return \Illuminate\Database\Eloquent\Builder
     */

   
public function scopeOfType($query, $type)
   
{
       
return $query->where('type', $type);
   
}
}

$users = App\User::ofType('admin')->get();

Hence, IMO, none of your advantages are valid:
  1. scopeOfType is not the real method; it is a method used to build/provide an ofType method
  2. does more harm than good, because of 1.
  3. does more harm than good, because of 1.

Having said that, nonetheless, I believe you are right. It makes most sense to document ofType as close as possible to scopeOfType:

class User extends Model
{
   
/**
     * Scope a query to only include users of a given type.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param mixed $type
     * @return \Illuminate\Database\Eloquent\Builder
     */

   
/**
     * @method $this ofType(mixed $type) {
     *     Only include users of a given type.
     *
     *     @param mixed $type
     *     @return $this
     * }
     */

   
public function scopeOfType($query, $type)
   
{
       
return $query->where('type', $type);
   
}
}

The only thing new about that would be the place; thus, if you want to navigate to the declaration of ofType, then your IDE is able to navigate to the @method comment – which is the most reasonable I can think of.

When I think about it, placing @method within the curly class brackets makes more sense to me, than placing it before the class keyword anyway.

Bests,
Michael

David Rodrigues

unread,
Apr 4, 2017, 7:26:33 AM4/4/17
to PHP Framework Interoperability Group
If we can use {}, then we can keep it on class.

/**
 * Class description.
 *
 * @method $this ofType($type) {
 *     Method magically specified by:
 *     @specification scopeOfType
 * }
 *
 * @method Something something() {
 *     Method magically specified by external class:
 *     @specification OtherClass::something

 * }
 *
 * @property User[]|Collection $users {
 *     Property magically declared by:
 *     @specification users
 * }
 */

Michael Mayer

unread,
Apr 4, 2017, 8:09:14 AM4/4/17
to PHP Framework Interoperability Group
On Tuesday, April 4, 2017 at 1:26:33 PM UTC+2, David Rodrigues wrote:
If we can use {}, then we can keep it on class.

I assume so, see 5.4. Inline PHPDoc; but I would still prefer to write the docs as close at possible to the related code – see my example above or this example:

class Magic
{
   
// consider a class with many, many LOC

   
/**
     * @method $this method1(mixed $type) {
     * }
     *
     * @method $this method2() {
     * }
     *
     * @method $this method3() {
     * }
     */

   
public function __call($method, $parameters)
   
{
       

   
}
}

This would make navigation easier, wouldn't it?

David Rodrigues

unread,
Apr 4, 2017, 3:09:26 PM4/4/17
to PHP Framework Interoperability Group
Yeah, make sense for me.
I should agree with you.
Reply all
Reply to author
Forward
0 new messages