Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time

Безболезненный рефакторинг

"Статическая" типизация

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

  • извлечение методов и классов из других методов и классов;
  • переименование их, добавление и удаление параметров;
  • переключение с одного метода или класса на другой.

Современные среды разработки (IDE) имеют на борту кучу инструментов, которые облегчают рефакторинг, а иногда выполняют его полностью автоматически. Однако, динамическая природа PHP часто вставляет палки в колёса.

public function publish($id)
{
    $post = Post::find($id);
    $post->publish();
}

// или

public function makePublish($post)
{
    $post->publish();
}

В обоих этих случаях IDE не может самостоятельно понять, что был вызван метод publish класса Post. В случае необходимости добавить новый параметр в этот метод, нужно будет найти все использования этого метода.

public function publish(User $publisher)

IDE не сможет сама найти их. Разработчику придётся искать по всему проекту слово «publish» и найти среди результатов именно вызовы данного метода. Для каких-то более распространённых слов (name или create) и при большом размере проекта это может быть весьма мучительно.

Представим ситуацию когда команда обнаруживает, что в поле email в базе данных находятся значения, не являющиеся корректными email-адресами. Как это произошло? Необходимо найти все возможные присвоения полю email класса User и проверить их. Весьма непростая задача, если учесть, что поле email виртуальное и оно может быть присвоено вот так:

$user = User::create($request->all());
//or
$user->fill($request->all());

Эта автомагия, которая помогала нам так быстро создавать приложения, показывает свою истинную сущность, преподнося такие вот сюрпризы. Такие баги в продакшене иногда очень критичны и каждая минута важна, а я до сих пор помню, как целый день в огромном проекте, который длится уже лет 10, искал все возможные присвоения одного поля, пытаясь найти, где оно получает некорректное значение.

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

public function publish(Post $post)
{
    $post->publish();
}

// или с phpDoc

public function publish($id)
{
    /**
     * @var Post $post
     */
    $post = Post::find($id);
    $post->publish();
}

Комментарии phpDoc могут помочь и в сложных случаях:

/**
 * @var Post[] $posts
 */
$posts = Post::all();
foreach($posts as $post) {
    $post->// Здесь IDE должна подсказывать
           // все методы и поля класса Post
}

Подсказки IDE приятны при написании кода, но намного важнее, что подсказывая их, она понимает откуда они и всегда найдёт их использования.

Если функция возвращает объект какого-то класса, он должен быть объявлен как return-тип (начиная с PHP7) или в @return теге phpDoc-комментария функции.

public function getPost($id): Post
{
    //...
}

/**
 * @return Post[] | Collection
 */
public function getPostsBySomeCriteria(...)
{
    return Post::where(...)->get();
}

Меня пару раз спрашивали: зачем я делаю Java из PHP? Я не делаю, я просто создаю маленькие комментарии, чтобы иметь удобные подсказки от IDE прямо сейчас и огромную помощь в будущем, при рефакторинге и дебаггинге. Даже для небольших проектов они невероятно полезны.

Шаблоны

На сегодняшний день все больше и больше проектов имеют только API-интерфейс, однако количество проектов, напрямую генерирующих HTML все ещё велико. Они используют шаблоны, в которых тоже много вызовов методов и полей. Типичный вызов шаблона в Laravel:

return view('posts.create', [
    'author' => \Auth::user(),
    'categories' => Category::all(),
]);

Он выглядит как вызов некоей функции. Сравните с этим псевдо-кодом:

/**
* @param User $author
* @param Category[] | Collection $categories
 */
function showPostCreateView(User $author, $categories): string
{
    //
}

return showPostCreateView(\Auth::user(), Category::all());

Хочется так же описать и параметры шаблонов. Это легко, когда шаблоны написаны на чистом PHP — комментарии phpDoc работают как и везде. Для шаблонных движков, таких как Blade, это не так просто и зависит от IDE. Я работаю в PhpStorm, поэтому могу говорить только про него. С недавних пор там тоже можно объявлять типы через phpDoc:

<?php
/**
 * @var \App\Models\User $author
 * @var \App\Models\Category[] $categories
 */
?>

@foreach($categories as $category)
    {{$category->//Category class fields and methods autocomplete}}
@endforeach

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

Поля моделей

Использование магических методов __get, __set, __call и других соблазнительно, но опасно, как пение сирен — находить такие магические вызовы будет сложно. Если вы используете их, лучше снабдить эти классы нужными phpDoc комментариями. Пример с небольшой Eloquent моделью:

class User extends Model
{
    public function roles()
    {
        return $this->hasMany(Role::class);
    }
}

Этот класс имеет несколько виртуальных полей, представляющих поля таблицы users, а также поле roles. С помощью пакета laravel-ide-helper можно автоматически сгенерировать phpDoc для этого класса. Всего один вызов artisan команды и для всех моделей будут сгенерированы комментарии:

/**
 * App\User
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property-read Collection|\App\Role[] $roles
 * @method static Builder|\App\User whereEmail($value)
 * @method static Builder|\App\User whereId($value)
 * @method static Builder|\App\User whereName($value)
 * @mixin \Eloquent
 */
class User extends Model
{
    public function roles()
    {
        return $this->hasMany(Role::class);
    }
}

$user = new User();
$user->// Здесь IDE подскажет все поля!

Возвратимся к примеру из прошлой главы:

public function store(Request $request, ImageUploader $imageUploader) 
{
    $this->validate($request, [
        'email' => 'required|email',
        'name' => 'required',
        'avatar' => 'required|image',
    ]);
    
    $avatarFileName = ...;    
    $imageUploader->upload($avatarFileName, $request->file('avatar'));
        
    $user = new User($request->except('avatar'));
    $user->avatarUrl = $avatarFileName;
    
    if (!$user->save()) {
        return redirect()->back()->withMessage('...');
    }
    
    \Email::send($user->email, 'Hi email');
        
    return redirect()->route('users');
}

Создание сущности User выглядит странновато. До некоторых изменений оно выглядело хотя бы красиво:

User::create($request->all());

Потом пришлось его поменять, поскольку поле avatarUrl нельзя присваивать напрямую из объекта запроса.

$user = new User($request->except('avatar'));
$user->avatarUrl = $avatarFileName;

Оно не только выглядит странно, но и небезопасно. Этот метод используется в обычной регистрации пользователя. В будущем может быть добавлено поле admin, которое будет выделять администраторов от обычных смертных. Какой-нибудь сообразительный хакер может просто сам добавить новое поле в форму регистрации:

<input type="hidden" name="admin" value="1"> 

Он станет администратором сразу же после регистрации. По этим причинам некоторые эксперты советуют перечислять все нужные поля (есть ещё метод $request->validated(), но его изъяны будут понятны позже в книге, если будете читать внимательно):

$request->only(['email', 'name']);

Но если мы и так перечисляем все поля, может просто сделаем создание объекта более цивилизованным?

$user = new User();
$user->email = $request['email'];
$user->name = $request['name'];
$user->avatarUrl = $avatarFileName;

Этот код уже можно показывать в приличном обществе. Он будет понятен любому PHP-разработчику. IDE теперь всегда найдёт, что в этом месте полю email класса User было присвоено значение.

«Что, если у сущности 50 полей?» Вероятно, стоит немного поменять интерфейс пользователя? 50 полей - многовато для любого, будь то пользователь или разработчик. Если не согласны, то дальше в книге будут показаны пару приемов, с помощью которых можно сократить данный код даже для большого количества полей.

Laravel Idea

Чтобы показать, насколько это для меня является важным, я опишу пару фич из плагина, который я разрабатываю для PhpStorm - Laravel Idea.

User::where('email', $email);

Плагин виртуально свяжет строку 'email' в этом коде с полем $email класса User. Это позволяет подсказывать все поля сущности для первого аргумента метода where, а также находить все подобные использования поля $email и даже автоматически переименовать все такие строки, если пользователь переименует $email в какой-нибудь $firstEmail. Это работает даже для сложных случаев:

Post::with('author:email');

Post::with([
    'author' => function (Builder $query) {
        $query->where('email', 'some email');
    }]);

В обоих этих случаях PhpStorm найдёт, что здесь было использовано поле $email. То же самое с роутингом:

Route::get('/', 'HomeController@index');

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

Мы сделали наш код более удобным для будущих рефакторингов или дебага. Эта «статическая типизация» не является обязательной, но она крайне полезна. Необходимо хотя бы попробовать.