Восемь причин изучить PHPDoc

Часто встречаю вопрос о том, что же это за странные блоки комментариев постоянно генерируются в представлениях:

<?php
/* @var $this yii\web\View */
/* @var $searchModel \app\models\search\UserSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
?>

в ActiveRecord-моделях:

/**
 * This is the model class for table "{{%user}}".
 *
 * @property integer $id
 * @property string $username
 * @property string $auth_key
 * @property string $password_hash
 * @property string $email
 */
class User extends ActiveRecord
{
    ...
}

и перед всеми методами:

/**
 * Saves the current record.
 
 * @param boolean $runValidation whether to perform validation before saving the record.
 * @param array $attributeNames list of attribute names that need to be saved.
 * @return boolean whether the saving succeeded (i.e. no validation errors occurred).
 */
public function save($runValidation = true, $attributeNames = null)
{
    ...
}

Что они обозначают и зачем они нужны? Это какой-то особый синтаксис объявления переменных в PHP или что?

Нет, это не совсем синтаксис PHP. А точнее, к самому PHP он никакого отношения не имеет и сам PHP интерпретатор его никогда не парсит. Это PHPDoc-блок, зародившийся ещё как JavaDoc и перешедший в PHPDocumentor

Пока мы не кодим так, как зажигают эти чуваки:

но попробуем разобраться.

Все эти вещи, судя по названию, как-то связаны с документацией. С неё и начнём.

Документация кода

Кому то стало лень писать документацию отдельно и он, вероятно, решил: «Отдельно документировать проект сложно и муторно. Нам же проще описывать прямо в комментариях каждый метод и класс. Давайте сделаем особый вид комментариев и при необходимости будем генерировать документацию по нему». И понеслось. В итоге придумали конструкции для объявления переменных, их типов и прочей метаинформации. И написали автоматический генератор, который парсит файлы в папке и генерирует HTML-файлы для каждого нашего класса.

Если посмотреть в API Reference и в код класса то увидим одно и то же:

namespace yii\db;
 
/**
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
 *
 * ...
 *
 * As an example, say that the `Customer` ActiveRecord class is associated with the `customer` table.
 * This would mean that the class's `name` attribute is automatically mapped to the `name` column in `customer` table.
 * Thanks to Active Record, assuming the variable `$customer` is an object of type `Customer`, to get the value of
 * the `name` column for the table row, you can use the expression `$customer->name`.
 * In this example, Active Record is providing an object-oriented interface for accessing data stored in the database.
 * But Active Record provides much more functionality than this.
 *
 * To declare an ActiveRecord class you need to extend [[\yii\db\ActiveRecord]] and
 * implement the `tableName` method:
 *
 * ```php
 * <?php
 *
 * class Customer extends \yii\db\ActiveRecord
 * {
 *     public static function tableName()
 *     {
 *         return 'customer';
 *     }
 * }
 * ```
 * ...
 */
class ActiveRecord extends BaseActiveRecord
{
    ...
}

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

Так что можем уже сформулировать первый бонус, который нам дают PHPDoc-блоки:

Применение первое: Возможность одной командой в консоли сгенерировать документацию с описанием всех классов, полей и методов своего проекта.

Но кроме этого чем он полезен в реальной жизни? Рассмотрим ещё несколько применений.

Виртуальные поля

В модели он пригодится для той же автогенерации по классу этой модели, но для чего его используют в предствалениях, которые в документации не выводятся?

Оказалось, что формат PHPDoc оказался настолько удачным, что его встроенную поддержку наряду с JavaDoc подхватили практически все IDE вроде PhpStorm, NetBeans и прочие. В PHP нет встроенной типизации для чисел и строк, поэтому указывать тип в комментарии (чтобы не забыть) было бы полезно. Вот и нашему PHP-редактору оттуда тоже оказалось удобно парсить переменные и их типы.

Например, был класс без полей:

class Post extends ActiveRecord
{
    public static function tableName() {
        return '{{%post}}';
    }
}

и непонятно что там, так как Yii2 берёт атрибуты из таблицы в базе данных. Автоподстановка видит только поля и методы из базового класса ActiveRecord:

А наши поля подчёркивает, при этом ругаясь, что их в объекте нет:

Можно заморочиться и написать плагин, который бы парсил поля из базы для каждой модели. Но это явно не лёгкий путь.

А давайте просто обозначим поля в PHPDoc-блоке (комментарии особой формы):

/**
 * @property integer $id
 * @property integer $user_id
 * @property integer $category_id
 * @property integer $created_at
 * @property integer $updated_at
 * @property string $title
 * @property string $content
 * @property integer $status
 */
class Post extends ActiveRecord
{
    public static function tableName() {
        return '{{%post}}';
    }
}

и редактору сразу станет понятно, какие поля теперь там есть для автоподстановки и какого они типа:

Теперь он в курсе наших дел и больше не ругается.

Или если в модели есть связи:

/**
 * @property integer $id
 * @property integer $user_id
 * @property integer $category_id
 * @property string $title
 * @property string $content
 */
class Post extends ActiveRecord
{
    public function getCategory()
    {
        return $this->hasOne(Category::className(), ['id' => 'category_id']);
    }
 
    public function getUser()
    {
        return $this->hasOne(Category::className(), ['id' => 'category_id']);
    }
 
    public function getTags()
    {
        return $this->hasMany(Tag::className(), ['id' => 'tag_id'])->viaTable(PostTag::tableName(), ['post_id' => 'id']);
    }
}

В коде мы можем использовать их просто как поля category, user и tags:

echo $post->category->name;
 
echo $post->user->username;
 
foreach ($post->tags as $tag) {
    echo $tag->name;
}

Но этих полей в классе нет. Всё работает через виртуальные методы и геттеры. Поэтому нам ничего не остаётся, как указать эти псевдополя и их типы явно:

/**
 * @property integer $id
 * @property integer $user_id
 * @property integer $category_id
 * @property string $title
 * @property string $content
 
 * @property User $user
 * @property Category $category
 * @property Tag[] $tags
 */
class Post extends ActiveRecord
{
    ...
}

Так что PHPDoc-блок перед классом может содержать полное перечисление того, чего в классе нет.

Применение второе: Указание псевдополей класса.

В примерах мы рассмотрели пока только поля. К псевдометодам подойдём позже.

Типы существующих полей

Помимо блока перед классом можно использовать и другие места.

Допустим, что у нас есть приватное поле _user у класса PasswordChangeForm:

class PasswordChangeForm extends Model
{
    ...
 
    private $_user;
 
    public function __construct($user, $config = [])
    {
        $this->_user = $user;
        parent::__construct($config);
    }
 
    ...
 
    public function changePassword()
    {
        if ($this->validate()) {
            $user = $this->_user;
            $user->setPassword($this->newPassword);
            return $user->save();
        } else {
            return false;
        }
    }
}

Но на строках:

$user = $this->_user;
$user->setPassword($this->newPassword);
return $user->save();

редактор впадает в ступор и методы setPassword и save подсвечивает жёлтым, мотивируя это тем, что таких методов в этой переменной нет:

А если мы подскажем, что там у нас находится модель класса User:

class PasswordChangeForm extends Model
{
    /**
     * @var User
     */
    private $_user;
}

то всё заработает как нам этого и хотелось.

Здесь мы аннотации @var передаём только тип. Но вообще ей можно передавать имя переменной, её тип или оба аргумента сразу.

Применение третье: Подсказка типов для имеющихся полей в классе.

Тип возвращаемого результата

Аналогично, если мы аннотацией @return укажем тип возвращаемого методом объекта:

class UsersController extends Controller
{
    /**
     * @param integer $id
     * @return User
     * @throws NotFoundHttpException
     */
    protected function findModel($id)
    {
        if (($model = User::findOne($id)) !== null) {
            return $model;
        } else {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    }
}

то при использовании этого метода наша система разработки поймёт, что внутри переменной $user после вызова данного метода окажется модель класса User:

class UsersController extends Controller
{
    public function actionUpdate($id)
    {
        $model = $this->findModel($id);
 
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    }
}

и больше не будет ругаться на ранее неизвестные ей методы load и save.

А что если какой-либо метод возвращает нам либо User, либо null? Тогда можно перечислить варианты через вертикальную черту:

/**
 * @param integer $id
 * @return User|null
 */

И IDE будет учитывать оба случая.

Применение четвёртое: Подсказка типов аргументов и типа возвращаемого результата (при наличии) у процедур, функций, методов.

Переменные из ниоткуда

Или ещё вариант. Есть некое представление, в которое из контроллера передаётся переменная $model. К тому же, это представление рендерится в объекте класса \yii\web\View, который и будет доступен через $this:

<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
 
$this->title = $model->meta_title;
$this->registerMetaTag(['name' => 'description', 'content' => $model->meta_description]);
$this->registerMetaTag(['name' => 'keywords', 'content' => $model->meta_keywords]);
 
$this->params['breadcrumbs'][] = ['label' => 'Блог', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="user-view">
 
    <h1><?= Html::encode($this->title) ?></h1>
 
    ...
 
</div>

Но IDE не в курсе наших планов, поэтому сразу подсвечивает переменные $this и $model как не определённые в этом файле. Для исправления ситуации мы можем добавить сколько угодно Doc-блоков с аннотацией @var прямо внутрь кода представления:

<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
 
/** @var $this \yii\web\View */
/** @var $model \app\models\User */
 
$this->title = $model->meta_title;
...
?>

При этом для @var можно указать сначала тип, потом имя переменной:

/** @var \app\models\User $model */

В итоге автоподстановка, поиск и автозамена полей и методов объектов заработают для этих переменных автоматически.

Применение пятое: Обозначение переменных, каким-либо образом переданных извне.

Подмена типа

Не всегда мы в контроллерах и прочих компонентах используем метод findModel. Часто можно напрямую в коде выполнить какой-нибудь запрос:

$model = Post::find()->where(['id' => $id])->andWhere(...)->one();
 
echo $model->title;

В итоге IDE по цепочке наследования подсмотрит аннотации метода ActiveRecord::find:

class ActiveRecord extends BaseActiveRecord
{
    /**
     * @inheritdoc
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
     */
    public static function find()
    {
        return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
    }

и, так как указана аннотация @inheritdoc, пойдёт ещё выше в аннотации этого же метода в интерфейсе:

interface ActiveRecordInterface
{
    /**
     * Creates an ActiveQueryInterface instance for query purpose.
     * ...
 
     * @return ActiveQueryInterface the newly created [[ActiveQueryInterface]] instance.
     */
    public static function find();
}

и склеит всё воедино.

В итоге IDE по строке:

/**
 * @return ActiveQuery
 */

поймёт, что из метода find должен вернуться экземпляр класса \yii\db\ActiveQuery и методы where и one будут вызываться уже у него:

lass ActiveQuery extends Query implements ActiveQueryInterface
{
    ...
 
    /**
     * Executes query and returns a single row of result.
     * @param Connection $db the DB connection used to create the DB command.
     * @return ActiveRecord|array|null a single row of query result.
     */
    public function one($db = null)
    {
        ...
    }
}

и здесь мы видим, что метод one возвращает либо экземпляр ActiveRecord, либо массив (если вызывали asArray()), либо null. Редактор так и будет думать, поэтому выдаст замечание при попытке обратиться к полю title:

$model = Post::find()->where(['id' => $id])->one();
 
echo $model->title;

Для правильной работы нам нужно явно указать тип переменной с помощью Doc-блока перед присваиванием:

/** @var \app\models\Post $model */
$model = Post::find()->where(['id' => $id])->one();
 
echo $model->title;

или непосредственно перед использованием объекта:

$model = Post::find()->where(['id' => $id])->one();
 
/** @var \app\models\Post $model */
echo $model->title;

Это удобно делать в циклах по различным выборкам:

foreach (Category::find()->orderBy('name')->each() as $category) {
    /** @var \app\models\Category $category */
    echo $category->title;
}

Применение шестое: Подмена типа уже существующих переменных.

И вернёмся к методам.

Подключение примесей

Вначале мы рассмотрели виртуальные переменные. Теперь предположим, что к своей модели мы подключаем любое поведение вроде:

composer require yii-dream-team/yii2-upload-behavior

Его достаточно добавить в метод behaviors модели:

/**
 * @property integer $id
 * @property integer $user_id
 * @property integer $category_id
 * @property string $title
 * @property string $content
 * @property string $image
 */
class Post extends ActiveRecord
{
    ...
 
    public function behaviors()
    {
        return [
            [
                'class' => ImageUploadBehavior::className(),
                'createThumbsOnRequest' => true,
                'attribute' => 'image',
                'filePath' => '@webroot/uploads/posts/[[pk]].[[extension]]',
                'fileUrl' => '@web/uploads/posts/[[pk]].[[extension]]',
                'thumbPath' => '@webroot/uploads/posts/[[profile]]_[[pk]].[[extension]]',
                'thumbUrl' => '@web/uploads/posts/[[profile]]_[[pk]].[[extension]]',
                'thumbs' => [
                    'thumb' => ['width' => 100, 'height' => 100],
                    'preview' => ['width' => 250, 'height' => 180],
                ],
            ],
        ];
    }
}

и в представлениях выводить оригинал или превью:

<?= Html::img($post->getImageFileUrl('image')) ?>
<?= Html::img($post->getThumbFileUrl('image', 'preview')) ?>

А мы помним, что IDE ругается на всё, чего нет в классе. Но с помощью аннотации @mixin, которую поддерживает IDE PhpStorm и, возможно, некоторые другие, можно «подмешать» класс поведения:

/**
 * @property integer $id
 * @property integer $user_id
 * @property integer $category_id
 * @property string $title
 * @property string $content
 * @property string $image
 *
 * @mixin ImageUploadBehavior
 */
class Post extends ActiveRecord
{
    ...
}

и все методы getImageFileUrl и прочие будут доступны в автоподстановке уже нашей модели.

Но есть один нюанс. Помимо нужных методов в классе поведения имеется и много ненужных. Например, вспомогательные resolveProfilePath или createThumbs, которые мы использовать не будем.

В таком случае вместо примешавания всего класса поведения с помощью @mixin мы можем просто добавить определение только пары нужных нам виртуальных методов:

/**
 * @property integer $id
 * @property integer $user_id
 * @property integer $category_id
 * @property string $title
 * @property string $content
 * @property string $image
 *
 * @method getImageFileUrl($attribute, $emptyUrl = null)
 * @method getThumbFileUrl($attribute, $profile = 'thumb', $emptyUrl = null)
 */
class Post extends ActiveRecord
{
    ...
}

Аналогично можно добавлять сигнатуры любых своих методов, работающих через свой магический метод __call.

Применение седьмое: Определение псевдометодов класса.

Программирование с аннотациями

Помимо простой работы с PHPDocumentor некоторые системы пошли дальше и придумали для своих целей собственные виды аннотаций. И некий программный код через рефлексию парсит эти данные. Взять хоть первый попавшийся пример с сайта StackOverflow:

$r = new ReflectionClass($class);
$doc = $r->getDocComment();
preg_match_all('#@(.*?)\n#s', $doc, $annotations);
print_r($annotations[1]);

Аналогично можно получить $r->getMethods() и парсить уже их.

В итоге этот подход нашёл новое применение. Например, в Symfony Framework с помощью собственных аннотаций (помимо конфигурации в YAML или XML-файлах) можно конфигурировать те же сущности прямо в коде:

namespace AppBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="product")
 */
class Product
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
 
    /**
     * @ORM\Column(type="string", length=100)
     */
    protected $name;
 
    /**
     * @ORM\Column(type="decimal", scale=2)
     */
    protected $price;
 
    /**
     * @ORM\Column(type="text")
     */
    protected $description;
}

или ту же маршрутизацию контроллеров:

class BlogController extends Controller
{
    /**
     * @Route("/blog/{slug}", name="blog_show")
     */
    public function showAction($slug)
    {
        // ...
    }
}

И фреймворк легко парсит эти данные и кеширует в простые PHP-массивы, чтобы не парсить их каждый раз снова и снова.

Также в уроке по тестированию мы рассматривали аннотации вроде @group и @dataProvider для тестов в пакете PHPUnit.

Этот подход уже мало связан с оригинальным PHPDoc, так как просто использует его идею. Но при этом друг другу никто не мешает и можно спокойно использовать различные ключи вместе:

class Product
{
    /**
     * @var string
     * @ORM\Column(type="string", length=100)
     */
    protected $name;
}

Многим такой подход с конфигурированием аннотациями в Symfony не нравится, так как он нарушает принцип единой ответственности, смешивая программный код и конфигурацию в одном файле. Так что используйте по своему усмотрению.

Восьмая причина: Появление программных систем с поддержкой специфических аннотаций.

Пока на этом всё. Если в комментариях приведут и другие примеры использования, то дополню статью.

И в итоге

С помощью PHPDoc-блоков можно легко «научить» свой редактор понимать вас с полуслова и всегда знать, какие переменные и каких типов используются в каждой строчке кода.

После этого не будет проблем с автоподстановкой, опечатками, несовпадениями типа, неопределёнными переменными, с автоматическим переименованием полей или методов любого класса и прочим автоматическим рефакторингом.

Дружите со своей IDE и она облегчит вашу жизнь.

Ну и, вроде, праздник скоро, так что не спалите ёлку:

Другие статьи

В прошлой части мы немного изменили структуру нашей системы. Фактически это были банальные операции по переносу файлов и частей кода с места на место. А сейчас добавим что-то новое. В комментариях предложили вычищать из базы пользователей, не активировавших свой адрес при регистрации. Рассмотрим. как это можно осуществить.

В нашем проекте хоть и есть модульная структура, но она пока модульная не полностью. Предыдущие главы были немного дополнены и изменены. Если вы следили за обновлениями в комментариях, то могли заметить, что мы немного исправили наш проект. Сегодня мы как раз рассмотрим эти исправления.

В этот раз порассуждали о понятиях и реализациях различных подходов к авторизации, аутентификации и контроле доступа на основе ролей в Yii2. Рассмотрели нюансы, сравнили друг с другом различные подходы к реализации RBAC.

Самая объёмная тема среди предложенных завершена. Выкладываю исправленную и дополненную запись вебинара-скринкаста о тестировании c PHPUnit и Codeception. Добавлены и доработаны примеры кода, пункты про аннотации, фикстуры, анализ покрытия, Faker, про установку всего через Composer и другие нюансы.

Комментарии

 

Артём

Здравствуйте!
Подскажите, пожалуйста, где описывать собственные компоненты, чтобы PhpStorm не подчёркивал их при вызове Yii::$app->myComponent->myMethod
Спасибо.

Ответить

 

Дмитрий Елисеев

В простейшем случае можно получить компонент явно:

/** @var \app\components\MyComponent $component */
$component = Yii::$app->get('myComponent');
$component->myMethod();
Ответить

 

Артём

Это понятно, но это не удобно.

Ответить

 

Дмитрий Елисеев

Тогда придумайте другой способ.

Ответить

 

Андрей

Для этого в нашем проекте мы создали файлик codeAssist.php следующего содержания:

/**
 * Класс приложения для работы подсказок IDE
 *
 * @property \common\components\ShopSetting $shopSettings Компонент настроек для магазина. Свойство только для чтения
 * @property \common\components\Setting $settings Компонент настроек платформы. Свойство только для чтения
 */
abstract class CodeAssistApplication extends Application
{
}

/**
 * Вспомогательный класс для работы подсказок IDE
 *
 */
class Yii extends BaseYii
{
    /**
     * @var \yii\console\Application|\yii\web\Application|CodeAssistApplication the application instance
     */
    public static $app;
}
Ответить

 

Roman Dukuy

как использовать ваш файл?

Ответить

 

Андрей

Просто положить в папку с проектом. У нас он лежит здесь:
backend/runtime/codeAssist.php

Ответить

 

Дмитрий Елисеев

А как борться теперь с конфликтом вроде multiple definition for class?

Ответить

 

Андрей

Ну, если речь о phpStorm'е идет, то кликаем правой кнопкой по vendor/yiisoft/yii2/Yii.php и выбираем в меню "Mark as Plain Text"

Ответить

 

Roman Dukuy

Да, но после пересборки vendor все заново делать

Ответить

 

Maxxi

Для этого можно унаследовать свой Application, в котором в виде хинтов @property указать свои компоненты. Не знаю, на сколько это целесообразно, но есть такой рецепт. :)

Ответить

 

Рома

Было бы ещё полезно узнать как автоматически генерировать API documents с возможностью пробовать в работе методы

Ответить

 

Никита

А вот как scopes прокомментировать, что бы ide потом подставила ключи массивов, как функцию или так не реально сделать?

Ответить

 

Никита

Забыл добавить что речь идет про Yii1)

Ответить

 

Никита

А вот как scopes прокомментировать, что бы ide потом подставила ключи массивов, как функцию или так не реально сделать?

Ответить

 

Дмитрий Елисеев

Также через @method у класса модели.

Ответить

 

Никита

А все понял спасибо)

Ответить

 

slo_nik

Интересно.
Но я пользуюсь sublime text 3, есть для этого редактора такая возможность?

Ответить

 

Arthur Mr

Что мешает проверить?

Ответить

 

Arthur Mr

Спасибо за статью, как всегда замечательно! "@mixin" я как-то пропустил, теперь буду использовать)

Заметил небольшие опечатки в нескольких местах: в комментарии "$post", а в коде "$model".

Ответить

 

Roman Dukuy

А что сделать чтоб не было подсветки серым "Field accessed via magic methot"

Yii::$app->conf->

Ответить

 

Дмитрий Елисеев

В первом комментарии уже, вроде, ответили.

Ответить

 

Иван

А как можно описать анонимную функцию, интересует конкретно конструкция use, например:

/**
 * @param $model Devices
 * @return string
 */
function ($model) use ($target_id, $dev_id, $id_def){
     return Html::a('<span class="glyphicon glyphicon-ok"></span>',
          ['dt-enquiry-devices/create', 'id' => $model->id, 'id_doc' => $target_id, 'dev_id' => $dev_id, 'id_def' => $id_def]);
}
Ответить

 

Дмитрий Елисеев
/**
 * @param $model Devices
 * @return string
 */
function (Devices $model) use ($target_id, $dev_id, $id_def) {
    /** @var integer $target_id */
    return ...;
}
Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>