Часто встречаю вопрос о том, что же это за странные блоки комментариев постоянно генерируются в представлениях:
<?php <?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
<?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 <?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 будут вызываться уже у него:
class 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],
],
],
];
}
}
и в представлениях выводить оригинал или превью:
<?php
<?= 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-файлах) с использованием пакета doctrine/annotations можно конфигурировать те же сущности прямо в коде:
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 не нравится, так как он нарушает принцип единой ответственности, смешивая программный код и конфигурацию в одном файле. Так что используйте по своему усмотрению.
Восьмая причина: Появление программных систем с поддержкой специфических аннотаций.