Это вторая часть перестройки Twitter с использованием Laravel. В Части 1 вы создаете фундамент платформы социальной сети. Теперь вы создаете для своего пользователя профиль и позволяете ему подписываться на других.

Если вы пропустили первую часть, вы можете прочитать ее здесь. В противном случае вы можете клонировать проект из Github. Я расскажу вам, как установить проект, прежде чем переходить ко второй части.

Если вы выполнили часть 1, сразу переходите к планированию.

Настройте проект из Github

Клонировать проект

Ссылка на проект: https://github.com/co0lsky/rebuild-twitter-with-laravel
Клонируйте проект из Github.

// Terminal
git clone -b '#1_User_and_Authentication' https://github.com/co0lsky/rebuild-twitter-with-laravel.git laratweet
cd laratweet
composer install

Настроить файл среды приложения

Продублируйте .env.example в .env.

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

// .env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laratweet
DB_USERNAME=homestead
DB_PASSWORD=secret

Создать ключ приложения

Затем вы должны сгенерировать ключ приложения. Ключ приложения помогает защитить сеансы пользователя вашего приложения и другие зашифрованные данные.

// Terminal
php artisan key:generate
Application key [base64:uJwG9Kge1xwH7O0/sckwN96pENJJy8cr5i+WbwQ7dYw=] set successfully.

Миграция

Затем перенесите вашу базу данных.

// Terminal
php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

Тестовое задание

Запустите ваше приложение.

Зарегистрируйтесь как пользователь.

Хорошо, приложение готово.

Планирование

Twitter позволяет вам подписаться на пользователя или отписаться от него на странице его профиля.

В этом случае вы собираетесь создать страницу профиля с функцией подписки или отмены подписки.

Это страницы, которые вы будете развивать,

  • Страница профиля
  • Следующая страница (список пользователей, на которых вы подписаны)
  • Страница подписчиков (список подписчиков на вас)

Хорошо, давайте подведем итоги вашего планирования

  • создать страницу профиля
  • добавить кнопку подписки / отмены подписки
  • создать страницы для подписчиков и подписчиков

Перед тем, как вы начнете, вашему приложению нужно новое имя. Имя приложения увеличивает ваше владение приложением. Давайте назовем его Laratweet (можете называть его как хотите).

Ларатвит

Чтобы переименовать приложение, эти файлы задействуют.

// config\app.php
'name' => 'Laratweet',
// resources\views\welcome.blade.php
<title>{{ config(‘app.name’, ‘Laravel’) }}</title>
…
<div class=”title m-b-md”>
    Laratweet
</div>
<div class=”links”>
    <a href=”https://medium.com/@just4sky/rebuild-twitter-with-laravel-user-and-authentication-9b0adb392dc6">User and Authentication</a>
</div>

Вам нравится новая страница приветствия? Не нравится? Не стесняйтесь изменять его.

Создадим страницу профиля.

Профиль

На первом этапе вы создаете новый контроллер, который находит модель User с именем пользователя и передает модель в представление.

// Terminal
php artisan make:controller ProfileController
Controller created successfully.

Затем введите новый маршрут для страницы профиля - / {username}.

// routes/web.php
Route::get('/{username}', 'ProfileController@show');

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

Создайте новый файл представления, продублировав файл resources / views / home.blade.php и переименуйте его в profile.blade.php.

Прежде чем передавать информацию в представление, давайте проведем небольшой тест, чтобы убедиться, что маршрут хорошо взаимодействует с ProfileController. Вы выводите параметр маршрута (имя пользователя) из URL-адреса.

// app/Http/Controllers/ProfileController.php
public function show($username)
{
    return view('profile', ['username' => $username]);
}
// resources/views/profile.blade.php
 <div class="panel-heading">Profile</div>
<div class="panel-body">
    {{ $username }}
</div>

Хорошо, попробуйте просмотреть страницу профиля. В моем случае мой URL-адрес - http://laratweet.app/sky.

Когда вы видите, что имя пользователя отображается на странице, вы на правильном пути.

Я готовлю для вас страницу профиля с обновленной версией. Что за апгрейд?

  • Файл макета, который будет использоваться на странице профиля, подписчиков и подписчиков.
  • Файл просмотра, который покажет список твитов (пока список пуст)

Создайте файл макета с именем profile.blade.php в файлах resources / views / layouts. Скопируйте контент отсюда.

Отредактируйте ресурсы / views / profile.blade.php, скопировав содержимое отсюда.

Затем вы получаете информацию, необходимую для выполнения требований страницы профиля.

// app/Http/Controllers/ProfileController.php
use App\User;
…
public function show($username)
{
     $user = User::where('username', $username)->firstOrFail();
return view('profile', ['user' => $user]);
}

Страница профиля выглядит хорошо! Подождите, что будет, если вы просмотрите несуществующий профиль пользователя?

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

404 стр.

Говоря об обработке ошибок, Laravel - безмолвный герой, который делал все необходимые работы без вашего ведома. Доказательством является страница, которую вы просматриваете прямо сейчас, а именно настраиваемая страница ошибок, а не страница ошибок веб-сервера.

Что вам нужно сделать, так это создать 404.blade.php в файлах resources / views / errors. Вот файл, который я для вас приготовил.

Теперь страница профиля готова. Давайте перейдем к следующей функции, последователи.

Подписчики

В этой функции ключевой частью является дизайн таблицы базы данных.

Пользователь может подписаться на многих пользователей.
Многие пользователи могут подписаться на пользователя.

Это отношения «многие ко многим».

Нормализация базы данных или просто нормализация - это процесс организации столбцов (атрибутов) и таблиц (отношений) реляционной базы данных для уменьшения избыточности данных и улучшения целостности данных. - Википедия

Нормализация базы данных снижает избыточность данных, однако денормализация улучшает производительность чтения.

В реальных разработках денормализация - это распространенный метод уменьшения количества соединяемых таблиц в вашем запросе выбора.

Вы можете прочитать простое объяснение от Елены здесь.
Более подробно вы можете прочитать по этой ссылке.

Итак, вопрос нормализовать или нет?

На этом этапе, по моему мнению, ваша главная цель - добиться результата. Вы не тратите много времени на обсуждение нормализации без какого-либо тестирования.

Миграция

Сопровождающие таблицы служат в качестве сводной таблицы. В нем есть столбец с именем user_id, в котором хранится user.id, и другой столбец с именем follower_user_id, в котором хранится user.id подписчика.

follower_user_id следует за user_id.

Давайте создадим миграцию.

// Terminal
php artisan make:migration create_followers_table --create=followers
Created Migration: 2017_02_11_083844_create_followers_table
// database/migrations/2017_02_11_083844_create_followers_table.php
Schema::create('followers', function (Blueprint $table) {
    $table->integer('user_id')->unsigned()->index();
    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->integer('follower_user_id')->unsigned()->index();
    $table->foreign('follower_user_id')->references('id')->on('users')->onDelete('cascade');
    $table->timestamps();
});

onDelete («каскад») означает, что запись дочерней таблицы будет удалена при удалении внешнего идентификатора из родительской таблицы.

Внешний ключ с каскадным удалением означает, что при удалении записи в родительской таблице соответствующие записи в дочерней таблице будут автоматически удалены. В SQL Server это называется каскадным удалением. - TechOnTheNet

Выполните миграцию.

// Terminal
php artisan migrate
Migrated: 2017_02_11_083844_create_followers_table

Модель пользователя

Затем объявите связь между подписчиками и таблицей пользователей.

// app/User.php
/**
 * The following that belong to the user.
 */
public function following()
{
    return $this->belongsToMany('App\User', 'followers', 'follower_user_id');
}

Теперь таблица подписчиков пуста. Вам необходимо включить следующую функцию для генерации данных. Как? Создайте кнопку подписки на странице профиля.

Кнопка "Follow"

Когда пользователь нажимает кнопку «Follow», отправьте запрос контроллеру на выполнение следующего действия. Вам нужно 2 метода, которые следуют и отменяют подписку в контроллере.

Хотя у вас есть ProfileController, но вы не добавляете в него методы. Почему? ProfileController служит для поиска модели пользователя и предоставления необходимой информации для представления.

Вы создаете новый контроллер, который обслуживает действия пользователя.

// Terminal
php artisan make:controller UserController
Controller created successfully.

Создайте маршрут для последующего действия.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
    Route::get('/follows/{username}', 'UserController@follows');
    Route::get('/unfollows/{username}', 'UserController@unfollows');
});

Маршрут защищен аутентификацией (ПО промежуточного слоя). Гостевые пользователи не могут выполнять следующие действия без входа в систему.

Затем добавьте следующий метод в UserController.

// app/Http/Controllers/UserController.php
public function follows($username)
{
    // Find the User. Redirect if the User doesn't exist
    $user = User::where('username', $username)->firstOrFail();
// Find logged in User
    $id = Auth::id();
    $me = User::find($id);
    $me->following()->attach($user->id);
    return redirect('/' . $username);
}
public function unfollows($username)
{
    // Find the User. Redirect if the User doesn't exist
    $user = User::where('username', $username)->firstOrFail();
// Find logged in User
    $id = Auth::id();
    $me = User::find($id);
    $me->following()->detach($user->id);
    return redirect('/' . $username);
}

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

// resource/views/layouts/profile.blade.php
<div class="col-md-2">
@if (Auth::check())
    @if ($is_edit_profile)
    <a href="#" class="navbar-btn navbar-right">
        <button type="button" class="btn btn-default">Edit Profile</button>
    </a>
    @elseif ($is_follow_button)
    <a href="{{ url('/follows/' . $user->username) }}" class="navbar-btn navbar-right">
        <button type="button" class="btn btn-default">Follow</button>
    </a>
    @else
    <a href="{{ url('/unfollows/' . $user->username) }}" class="navbar-btn navbar-right">
        <button type="button" class="btn btn-default">Unfollow</button>
    </a>
    @endif
@endif
</div>

Вы проверите с помощью модели пользователя, подписан ли вошедший в систему пользователь на этого пользователя (просмотр профиля). Вы назначаете значения $ is_edit_profile и $ is_follow_button в ProfileController. $ is_edit_profile и $ is_follow_button помогают в просмотре определить, какая функция кнопки должна отображаться на странице.

// app/User.php
/**
 * The following that belong to the user.
 */
public function following()
{
    return $this->belongsToMany('App\User', 'followers', 'follower_user_id', 'user_id')->withTimestamps();
}
public function isFollowing(User $user)
{
    return !is_null($this->following()->where('user_id', $user->id)->first());
}
// app/Http/Controllers/ProfileController.php
public function show($username)
{
    $user = User::where('username', $username)->firstOrFail();
    $me = Auth::user();
    $is_edit_profile = (Auth::id() == $user->id);
    $is_follow_button = !$is_edit_profile && !$me->isFollowing($user);
    return view('profile', ['user' => $user, 'is_edit_profile' => $is_edit_profile, 'is_follow_button' => $is_follow_button]);
}

Попробуйте сами.

Вы здесь не закончили.

В настоящее время действие пользователя осуществляется путем отправки запроса GET, что не рекомендуется. Используя запрос GET, параметр становится видимым и менее безопасным. Вместо этого переключите его на запрос POST, чтобы скрыть параметр и повысить безопасность.

И не круто обновлять страницу каждый раз при выполнении действия, давайте изменим ее на вызов Ajax.

Переключите GET на POST

Начнем с представления.

// resource/views/layouts/profile.blade.php
<meta id="token" name="csrf-token" content="{{ csrf_token() }}”>
…
@if (Auth::check())
    @if ($is_edit_profile)
    <a href="#" class="navbar-btn navbar-right">
        <button type="button" class="btn btn-default">Edit Profile</button>
    </a>
    @else
    <button type="button" v-on:click="follows" class="navbar-btn navbar-right btn btn-default">@{{ followBtnText }}</button>
    @endif
@endif
…
<!-- <script src="/js/app.js"></script> -->
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue-resource.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data: {
            username: '{{ $user->username }}',
            isFollowing: {{ $is_following ? 1 : 0 }},
            followBtnTextArr: ['Follow', 'Unfollow'],
            followBtnText: ''
        },
        methods: {
            follows: function (event) {
                var csrfToken = Laravel.csrfToken;
                var url = this.isFollowing ? '/unfollows' : '/follows';
                this.$http.post(url, {
                    'username': this.username
                }, {
                    headers: {
                        'X-CSRF-TOKEN': csrfToken
                    }
                })
                .then(response => {
                    var data = response.body;
                    if (!data.status) {
                        alert(data.message);
                        return;
                    }
                    this.toggleFollowBtnText();
                });
            },
            toggleFollowBtnText: function() {
                this.isFollowing = (this.isFollowing + 1) % this.followBtnTextArr.length;
                this.setFollowBtnText();
            },
            setFollowBtnText: function() {
                this.followBtnText = this.followBtnTextArr[this.isFollowing];
            }
        },
        mounted: function() {
            this.setFollowBtnText();
        }
    });
</script>

Затем вы вносите небольшое изменение в ProfileController @ show, чтобы сообщить View, что текущий пользователь подписан на этого пользователя.

// app/Http/Controllers/ProfileController.php
public function show($username)
{
     $user = User::where('username', $username)->firstOrFail();
     $is_edit_profile = false;
     $is_following = false;
     if (Auth::check()) {
           $is_edit_profile = (Auth::id() == $user->id);
           $me = Auth::user();
           $is_following = !$is_edit_profile && $me->isFollowing($user);
     }
    return view('profile', ['user' => $user, 'is_edit_profile' => $is_edit_profile, 'is_following' => $is_following]);
}

Измените метод маршрута на публикацию.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
    Route::post('/follows', 'UserController@follows');
    Route::post('/unfollows', 'UserController@unfollows');
});

Затем вы принимаете объект Request, передаваемый в следующие, и отменяете подписку на методы и отвечаете на объект JSON.

// app/Http/Controllers/UserController.php
use Illuminate\Database\Eloquent\ModelNotFoundException;
...
public function follows(Request $request)
{
     $username =  $request->input('username');
     try {
          $user = User::where('username', $username)->firstOrFail();
     } catch (ModelNotFoundException $exp) {
          return $this->responseFail('User doesn\'t exists');
     }
     // Find logged in User
     $id = Auth::id();
     $me = User::find($id);
     $me->following()->attach($user->id);
     return $this->responseSuccess();
}
public function unfollows(Request $request)
{
     $username =  $request->input('username');
     try {
          $user = User::where('username', $username)->firstOrFail();
     } catch (ModelNotFoundException $exp) {
          return $this->responseFail('User doesn\'t exists');
     }
     // Find logged in User
     $id = Auth::id();
     $me = User::find($id);
     $me->following()->detach($user->id);
     return $this->responseSuccess();
}
private function responseSuccess($message = '')
{
     return $this->response(true, $message);
}
private function responseFail($message = '')
{
     return $this->response(false, $message);
}
private function response($status = false, $message = '')
{
     return response()->json([
          'status' => $status,
          'message' => $message,
          ]);
}

Что дальше? Следующие страницы и страницы подписчиков.

Страница подписок и подписчиков

Страницы, на которых подписаны и подписаны, имеют список имен пользователей. Они очень простые. Главное здесь - маршруты.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
    Route::get('/following', 'ProfileController@following');
    Route::post('/follows', 'UserController@follows');
    Route::post('/unfollows', 'UserController@unfollows');
});
Route::get('/{username}', 'ProfileController@show');
Route::get('/{username}/followers', 'ProfileController@followers');

Последовательность объявления маршрута важна. Вы можете понять маршрут профиля пользователя, / {username} размещается внизу. Эта последовательность связана с тем, что / follow и / {username} имеют аналогичный шаблон маршрута. Когда пользователь нажимает на адресную строку с помощью / follow, Laravel будет соответствовать маршруту сверху вниз.

Если ваши маршруты указаны ниже,

Route::get('/{username}', 'ProfileController@show');
Route::get('/{username}/followers', 'ProfileController@followers');
Route::group(['middleware' => 'auth'], function () {
    Route::get('/following', 'ProfileController@following');
    Route::post('/follows', 'UserController@follows');
    Route::post('/unfollows', 'UserController@unfollows');
});

Когда пользователь нажимает / follow в адресной строке, / {username} будет соответствовать вместо / follow. Вы не будете вводить / следующее.

Когда маршруты готовы, вы приступаете к работе с видами. Со страницей профиля можно сделать несколько вещей:

  • Показывать следующую вкладку, только если это ваш собственный профиль
  • Установить 2 новых маршрута
  • Показывать количество подписчиков и подписчиков
// resources/views/layouts/profile.blade.php
<div class="col-md-8">
 <ul class="nav navbar-nav">
  <li class="active">
      <a href="#" class="text-center">
          <div class="text-uppercase">Tweets</div>
          <div>0</div>
      </a>
  </li>
  @if ($is_edit_profile)
  <li>
      <a href="{{ url('/following') }}" class="text-center">
          <div class="text-uppercase">Following</div>
          <div>{{ $following_count }}</div>
      </a>
  </li>
  @endif
  <li>
      <a href="{{ url('/' . $user->username . '/followers') }}" class="text-center">
          <div class="text-uppercase">Followers</div>
          <div>{{ $followers_count }}</div>
      </a>
  </li>
 </ul>
</div>

Затем вы создаете 2 новых файла представления в resources / views.

// resources/views/following.blade.php
// The content refers to the https://github.com/co0lsky/rebuild-twitter-with-laravel/blob/%232_Followers/resources/views/following.blade.php

// resources/views/followers.blade.php
// The content refers to the https://github.com/co0lsky/rebuild-twitter-with-laravel/blob/%232_Followers/resources/views/followers.blade.php

Затем вы вводите новые отношения в модели User.

// app/User.php
/**
  * The followers that belong to the user.
  */
public function followers()
{
    return $this->belongsToMany('App\User', 'followers', 'user_id', 'follower_user_id')->withTimestamps();
}

Наконец, вы подсчитываете подписчиков и подписчиков в шоу ProfileController @. И добавьте ProfileController @ follow и ProfileController @ followers.

// app/Http/Controllers/ProfileController.php
public function show($username)
{
    $user = User::where('username', $username)->firstOrFail();
    $followers_count =  $user->followers()->count();
    $is_edit_profile = false;
    $is_following = false;
    if (Auth::check()) {
        $is_edit_profile = (Auth::id() == $user->id);
        $me = Auth::user();
        $following_count = $is_edit_profile ? $me->following()->count() : 0;
        $is_following = !$is_edit_profile && $me->isFollowing($user);
    }
    return view('profile', [
        'user' => $user,
        'followers_count' => $followers_count,
        'is_edit_profile' => $is_edit_profile,
        'following_count' => $following_count,
        'is_following' => $is_following
        ]);
}
public function following()
{
    $me = Auth::user();
    $followers_count = $me->followers()->count();
    $following_count = $me->following()->count();
    $list = $me->following()->orderBy('username')->get();
    $is_edit_profile = true;
    $is_following = false;
    return view('following', [
        'user' => $me,
        'followers_count' => $followers_count,
        'is_edit_profile' => $is_edit_profile,
        'following_count' => $following_count,
        'is_following' => $is_following,
        'list' => $list,
        ]);
}
public function followers($username)
{
    $user = User::where('username', $username)->firstOrFail();
    $followers_count =  $user->followers()->count();
    $list = $user->followers()->orderBy('username')->get();
    $is_edit_profile = false;
    $is_following = false;
    if (Auth::check()) {
        $is_edit_profile = (Auth::id() == $user->id);
        $me = Auth::user();
        $following_count = $is_edit_profile ? $me->following()->count() : 0;
        $is_following = !$is_edit_profile && $me->isFollowing($user);
    }
    return view('followers', [
        'user' => $user,
        'followers_count' => $followers_count,
        'is_edit_profile' => $is_edit_profile,
        'following_count' => $following_count,
        'is_following' => $is_following,
        'list' => $list,
        ]);
}

Ну вот!

Активный статус панели вкладок

Последнее, что нужно сделать, чтобы страница профиля выглядела отлично, - это правильно выделить вкладку навигации.

У меня есть 2 предложения,

  • Вы получаете текущий маршрут и выполняете сопоставление, какая бы вкладка ни совпадала, выделите ее.
  • Я рекомендую именованный маршрут, который делает обзоры более организованными.
// routes/web.php
Route::group(['middleware' => 'auth'], function () {
    Route::get('/following', 'ProfileController@following')->name('following');
    Route::post('/follows', 'UserController@follows');
    Route::post('/unfollows', 'UserController@unfollows');
});
Route::get('/{username}', 'ProfileController@show')->name('profile');
Route::get('/{username}/followers', 'ProfileController@followers')->name('followers');
// resources/views/layouts/profile.blade.php
<ul class="nav navbar-nav">
 <li class="{{ !Route::currentRouteNamed('profile') ?: 'active' }}">
     <a href="{{ url('/' . $user->username) }}" class="text-center">
         <div class="text-uppercase">Tweets</div>
         <div>0</div>
     </a>
 </li>
 @if ($is_edit_profile)
 <li class="{{ !Route::currentRouteNamed('following') ?: 'active' }}">
     <a href="{{ url('/following') }}" class="text-center">
         <div class="text-uppercase">Following</div>
         <div>{{ $following_count }}</div>
     </a>
 </li>
 @endif
 <li class="{{ !Route::currentRouteNamed('followers') ?: 'active' }}">
     <a href="{{ url('/' . $user->username . '/followers') }}" class="text-center">
         <div class="text-uppercase">Followers</div>
         <div>{{ $followers_count }}</div>
     </a>
 </li>
</ul>

Ta-da!

Прежде чем ты уйдешь

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

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

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

Не стесняйтесь оставлять свой ответ ниже или отправить мне письмо по адресу [email protected]

Что дальше?

Вы можете сделать две вещи

  1. Подпишитесь на мой список рассылки, я напишу вам, когда будет готов следующий пост.
  2. Если вы найдете это руководство полезным, поделитесь им со своим другом. Я благодарен вам, если вы это сделаете, я уверен, что ваш друг тоже вас поблагодарит.