yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.24k stars 6.9k forks source link

IDE auto-completion of module methods #16261

Closed rugabarbo closed 3 years ago

rugabarbo commented 6 years ago

What I'm using now

  1. I created my own component:
use yii\base\Component;
use yii\helpers\Inflector;

class ModuleAccessor extends Component
{
    private $_modules = [];

    public function __get($name)
    {
        if (!array_key_exists($name, $this->_modules)) {
            $moduleId = Inflector::camel2id($name);
            $this->_modules[$name] = \Yii::$app->getModule($moduleId);
        }

        return $this->_modules[$name];
    }
}
  1. Added the following lines to the app configuration:
   'components' => [
        'mod' => [
            'class' => \rugabarbo\packagename\components\ModuleAccessor::class,
        ],
   ]
  1. Added the following lines to the autocompletion.php:
/**
 * @property \rugabarbo\packagename\modules\Email\EmailModule $email
 */
class ModuleAccessor
{
}

/**
 * @property ModuleAccessor $mod
 */
abstract class BaseApplication extends yii\base\Application
{
}
  1. And now I call module methods like this:
\Yii::$app->mod->email->queue($to, $subject, $body, $params);

Why?

Need IDE autocompletion for module methods. Using of the syntax \Yii::$app->getModule('...')-> does not give such an opportunity.

What I suggest

Implement something similar to my ModuleAccessor at the kernel level ☺️

samdark commented 6 years ago

Can't we add same set of annotations to Module class to solve it? https://github.com/yiisoft/yii2/blob/master/framework/web/Application.php#L19

rugabarbo commented 6 years ago

@samdark how can it help?

samdark commented 6 years ago

The module would autocomplete standard set of components.

rugabarbo commented 6 years ago

The module would autocomplete standard set of components

I can not understand how this is related to auto-completion of module methods that are called by syntax \Yii::$app->getModule('...')->.

samdark commented 6 years ago

Directly. getModule() returns an instance of Module so it will autocomplete methods of the module. If the module has annotations for components, they will be autocompleted.

rugabarbo commented 6 years ago

@samdark, I'm sorry, but I still do not understand...

Suppose I have this method:

class EmailModule extends Module
{
    public function queue($to, $subject, $body, $params)
    {

    }
}

Suppose I configured the module like this:

'modules' => [
    'email' => [
        'class' => \namespace\for\EmailModule::class,
    ],
],

What annotations and where should I put in order to get an auto-completion of EmailModule::queue() method?

samdark commented 6 years ago

Right. That's custom module so it isn't possible on the framework level. Your solution is fine but at application level. Won't be part of the framework. Check https://github.com/samdark/yii2-cookbook/blob/master/book/ide-autocompletion.md as well.

rugabarbo commented 6 years ago

@samdark, I'm talking about the way of access to the methods of modules at the framework level. Syntax \Yii::$app->getModule('...')-> does not allow to use annotations for autocompletion. See my first post, please.

samdark commented 6 years ago

I see nothing in your first post that could be done in the framework itself. In order to have IDE completion for custom modules you have to make custom property annotations so you need to edit the file making it pointless to have it in the framework distribution.

rugabarbo commented 6 years ago

😞

Don't know how to explain in other way that method \Yii::$app->getModule('..') doesn't allow write annotations for concrete modules...

Will try later.

rugabarbo commented 6 years ago

@samdark

Александр, попробую ещё раз объяснить на русском. Может быть получится лучше, чем на английском.

Компоненты

Синтаксис доступа к компонентам у фреймворка такой:

\Yii::$app->componentName

Это позволяет нам писать в файле autocompletion.php для автокомплита такие аннотации:

/**
 * @property namespace\componentClass $componentName
 */
abstract class BaseApplication extends yii\base\Application
{
}

После этого мы можем без дополнительных PhpStorm-плагинов воспользоваться автодополнением для компонентов в IDE. Мы начинаем набирать \Yii::$app->componentName->, а IDE предлагает нам список методов, которые реализованы в компоненте componentName.

Здесь всё ОК.

Модули

Синтаксис доступа к модулям у фреймворка такой:

\Yii::$app->getModule('moduleName')

И это НЕ позволяет нам реализовать автокомплит конкретных модулей с помощью файла autocompletion.php. В этом и проблема.

В качестве хака можно сделать так:

/**
 * @method ModuleClass1|ModuleClass2|ModuleClass3|...|ModuleClassN getModule()
 */
abstract class BaseApplication extends yii\base\Application
{
}

Но проблема такого подхода в том, что IDE будет предлагать в качестве автодополнения методы всех указанных модулей одновременно – в моём примере это ModuleClass1, ModuleClass2, ModuleClass3 и т.д. Возникает путаница, это неудобно.

Что предлагаю

Предлагаю ввести дополнительный компонент по типу моего ModuleAccessor на уровне фреймворка, который обеспечит доступ к модулям с помощью синтаксиса:

\Yii::$app->mod->moduleName

Ключевое слово mod обсуждаемо. Его я выбрал для своих проектов, но на уровне фреймворка его можно заменить на что-то другое.


В общем, суть в том, что метод getModule('...') не позволяет использовать аннотации для автокомплита, и хочется решить эту проблему именно на уровне фреймворка.

Что думаешь?

samdark commented 6 years ago

Думаю, если так делать, придётся переделать много чего... Пока не вижу нормального способа. Твой — дублирование конфига, по сути. Не очень нравится.

rugabarbo commented 6 years ago

Но если мы не видим нормального способа, то это ведь не значит, что его нет. Нужно просто подумать. Я видел, что авторы модулей решают эту проблему разными кривыми способами. Или создают трейты с методом getModule() и соответствующим PhpDoc (примеры есть в issue #14421), или создают дополнительную локальную переменную:

/** @var moduleClass $module */
$module = \Yii::$app->getModule('name');
$module->...

Всё это хаки, которые не очень удобны.

uaoleg commented 3 years ago

@rugabarbo why MyModuleClass::getInstance() can't be used instead of \yii::$app->getModule('MyModule')?

rugabarbo commented 3 years ago

@rugabarbo why MyModuleClass::getInstance() can't be used instead of \yii::$app->getModule('MyModule')?

Because it forces you to refer to a specific class. Modules can be interchangeable. As a result, you bind to a specific class, not a module.

uaoleg commented 3 years ago

@rugabarbo I'd agree with you regarding components, but not modules

rugabarbo commented 3 years ago

Calling modules using Module::getInstance() is bad practice. The framework has a method \Yii::$app->getModule('MyModule') for this.

Method Module::getInstance() is intended for internal use within a module. But not for calling a module from an application. Please read the docs: https://www.yiiframework.com/doc/guide/2.0/en/structure-modules

If you want to bind to specific classes of modules in your application, then you can. But the modules were designed differently.

For example, in my projects some modules are interchangeable and I cannot use specific classes of modules in application. Moreover, there are applications that require checking whether the concrete module is installed or not. And in this case, using the class name of a module is also cannot be used...

samdark commented 3 years ago

Theoretically could be solved via PhpStorm meta files but overall, that's not a framework job since it involves actually mapping these annotations to real classes. Won't implement it.

uaoleg commented 3 years ago

The easiest and universal (cross-IDE) way is to create a helper class, kinda facades in Laravel:

<?php

class ModuleFacade
{

    public static function integrationFacebook(): \frontend\modules\integration\modules\facebook\Module
    {
        return \yii::$app->getModule('integration-facebook');
    }

    public static function integrationTelegram(): \frontend\modules\integration\modules\telegram\Module
    {
        return \yii::$app->getModule('integration-telegram');
    }

}

You'll need a separate helper for each of your applications - frontend, backend, console, etc.

rugabarbo commented 3 years ago

For me the easiest way is workaround which I suggested at the beginning of the thread. I still use this method.