Восемь причин изучить 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
будут вызываться уже у него:
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], ], ], ]; } }
и в представлениях выводить оригинал или превью:
<?= 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 не нравится, так как он нарушает принцип единой ответственности, смешивая программный код и конфигурацию в одном файле. Так что используйте по своему усмотрению.
Восьмая причина: Появление программных систем с поддержкой специфических аннотаций.
Пока на этом всё. Если в комментариях приведут и другие примеры использования, то дополню статью.
И в итоге
С помощью PHPDoc-блоков можно легко «научить» свой редактор понимать вас с полуслова и всегда знать, какие переменные и каких типов используются в каждой строчке кода.
После этого не будет проблем с автоподстановкой, опечатками, несовпадениями типа, неопределёнными переменными, с автоматическим переименованием полей или методов любого класса и прочим автоматическим рефакторингом.
Дружите со своей IDE и она облегчит вашу жизнь.
Ну и, вроде, праздник скоро, так что не спалите ёлку:
Здравствуйте!
Подскажите, пожалуйста, где описывать собственные компоненты, чтобы PhpStorm не подчёркивал их при вызове Yii::$app->myComponent->myMethod
Спасибо.
В простейшем случае можно получить компонент явно:
Это понятно, но это не удобно.
Тогда придумайте другой способ.
Для этого в нашем проекте мы создали файлик codeAssist.php следующего содержания:
как использовать ваш файл?
Просто положить в папку с проектом. У нас он лежит здесь:
backend/runtime/codeAssist.php
А как борться теперь с конфликтом вроде multiple definition for class?
Ну, если речь о phpStorm'е идет, то кликаем правой кнопкой по vendor/yiisoft/yii2/Yii.php и выбираем в меню "Mark as Plain Text"
Да, но после пересборки vendor все заново делать
Для этого можно унаследовать свой Application, в котором в виде хинтов @property указать свои компоненты. Не знаю, на сколько это целесообразно, но есть такой рецепт. :)
Почитайте документацию по расширенным метаданным в PHPStorm: https://confluence.jetbrains.com/display/PhpStorm/PhpStorm+Advanced+Metadata
Возможно, там найдется что-нибудь интересное.
Было бы ещё полезно узнать как автоматически генерировать API documents с возможностью пробовать в работе методы
А вот как scopes прокомментировать, что бы ide потом подставила ключи массивов, как функцию или так не реально сделать?
Забыл добавить что речь идет про Yii1)
А вот как scopes прокомментировать, что бы ide потом подставила ключи массивов, как функцию или так не реально сделать?
Также через @method у класса модели.
А все понял спасибо)
Интересно.
Но я пользуюсь sublime text 3, есть для этого редактора такая возможность?
Что мешает проверить?
Спасибо за статью, как всегда замечательно! "@mixin" я как-то пропустил, теперь буду использовать)
Заметил небольшие опечатки в нескольких местах: в комментарии "$post", а в коде "$model".
А что сделать чтоб не было подсветки серым "Field accessed via magic methot"
Yii::$app->conf->
В первом комментарии уже, вроде, ответили.
А как можно описать анонимную функцию, интересует конкретно конструкция use, например:
В нетбинсе такие конструкции, как указание типа локальной переменной (/** @var \app\models\Category $category */) или указания массивов в ява-стиле (@property Tag[] $tags) не работают. Да и в документации такое не описано. Так что это кастомные плюшки пхпшторма.
Обратил внимание, что "@mixin MyBehavior" не наследует свойства вроде "@property MyModel[] $myModel" прописанные прямо перед классом в поведении, при этом реальные методы поведения ide видит, это нормально и надо прописать такие свойства прямо в owner или есть решение без дублирования кода?
Тогда пропишите явно.
Подскажите, а как прописывать, например в том же slimphp есть такие конструкции $container['request']->getUri() и этот метод не видит шторм.. а сама переменная $container указана, как интерфейс ContainerInterface и все ок.. Спасибо!
Вот прям сердечное спасибо за статью - вроде ничего особенного, а в разработке приложения очень помогает (пользуюсь PHPStorm)!!!
/** @var \app\models\Post $model */
echo $model->title;
вот этот случай можно как-то обрулить через описание единоразово в моделе(возможно через переопределение чего-то). но я забыл как и нигде не могу найти( но я точно видел довольно простое решение, чтобы не писать это каждый раз.
Дмитрий, спасибо за столь подробную статью!
А можно ли как-нибудь добиться автоподстановок для полей объекта прямо внутри кода, а не при объявлении класса?
Например, мне известно, что в массиве $array имеется 3 элемента, которые я хочу сделать полями объекта:
Как прописать эти конкретные поля в аннотациях, чтобы IDE их предлагала для автоподстановки?
Вроде никак.
Добрый день.
У меня вот такой вопрос - на днях зашел разговор на работе о том на сколько уместно использовать сейчас phpdoc
Есть мнение, что это уже не настолько необходимо, т.к. IDE умеют читать и подставлять описание на основе типов входных параметров.
И что код должен быть гворящим сам за себя, т.е. он должен быть написан так, что не потребуется делать дополнительные комментарии и поэтому phpdoc пережиток прошлого.
Если кто - то пишет phpdoc, то это говорит о том, что это плохой программист.
В своих новых уроках, Вы не используете phpdoc, но это скорее в угоду скорости разработки.
Каких то холиваров по этому поводу я нигде в интернете не видел, в общем интересно Ваше мнение.
В PHP 7 появилась возможность указания скалярных типов аргументов, а сейчас в 7.4 и описание типов полей, поэтому простые блоки:
уже не нужны, так как это уже описывается непосредственно типами:
Сейчас PHPDoc остаётся нужен только для особых ситуаций вроде этой:
Здесь мы указываем, что это array именно из постов Post[] и что этот метод кидает исключение DomainException.
В языке Java это всё указывается типами:
а в PHP такого ещё нет.
А вот какие - то стандарты есть, которые предписывают использовать\не использовать комментарии?
Я ничего подобного не видел и получается, что данная практика уже больше на совести или желании команды?
К чему этот вопрос?
К тому, что если команда привыкла писать такие комментарии, то отучить ее уже будет сложно и в любом случае надо как -то аргументировтаь свое предложение.
Вот стандарт или какие то соглашения по типу PSR как раз в тему.
За типами следят статические анализаторы кода.
Как же мы жили раньше без этого?) На сегодня необходимо знать такое кол-во технологий, подходов, парадигм, языков..., каждая из которых обновляется раз в пару месяцев что просто физически невозможно следовать всем этим стандартам. Вникайте в это Doc только если вам заплатят деньги на фирме за это, или самому интересно. Я конечно не работал в гугле, но в тех компаниях где был, об этом даже не говорили (хотя в одной мы написали собственный фреймворк, который и кормит нас всех). Я вообще считаю что на документацию надо выделять отдельного человека, или доплачивать.
Комментирование в стиле PhpDoc - это очень удобно и помогает в больших проектах, особенно если используете IDE (например PhpStorm), который поддерживает это.
Я одно время программировал в Notepad++ и считал, что всякие там IDE вовсе не нужны и без них справляюсь, достаточно подсветки синтаксиса. Я так думал именно потому, что не работал с IDE. Когда начал осваивать и переходить PhpStorm, понял всю реальную прелесть в автоподстановке и подсказках. Когда обычным кликом по методу тебя перекидывает в файл и класс, который содержит этот метод. До этого мне приходилось искать этот файл, открывать, далее искать этот метод - сколько же лишних действий это создаёт, отвлекает от текущего файла. Также наведение мыши на метод - отображает подсказку, описание метода из PhpDoc, в скобочках отображает название параметров и их типы.
В MVC модели разделения данных во вьюшках, особенно когда их несколько и одна в другой подключается, теряется связь между переменными. Они переименовываются и не понятно какая что означает. Когда вверху файла объявишь через /** @var */ тип либо какую-то подсказку даёшь, как эта переменная формировалась, сразу же становится всё понятно и не надо возвращать своё внимание к самым истокам формирования и передачи переменной, чтобы найти все её концы во вьюшках.
Преимущество просто очевидное. Предполагаю, что вы не работает в IDE (PhpStorm либо ему подобных), иначе данного вопроса скорее всего просто бы не возникло.
То yii, то ларавель,... документация еще существует для тестов. самое важное и не упомянули.
А какое отношение документация имеет к тестам?
Дмитрий подскажите пожалуйста, в репозиториях хочется использовать код таким образом
Но при этом phpStorm выделяет этот код другим цветом, мол там есть несоответствие типов. Отображает такой нотис
Можно ли это как-то пофиксить?
Если я правильно понял, то всё это теперь решается неймспейсами и указанием типов для переменных. Да?
Дя, с PHP 8 теперь можно указывать типы и для полей. А PHPDoc можно оставить только для нестандартных типов вроде `Post[]` для массивов.