【Laravel】DM機能実装のメモ(DBリレーションから表示まで)

プログラミング初心者の勉強ブログ #127

LaravelでDM機能作成を試みたので、DBのリレーションから、コントローラ、ビューまでの実装についてのメモをまとめる。Laravelやってみて思うことは、基本はRailとほぼ同じなんだなということ。Laravelの方が実装しやすい部分もあれば、Railsの方がよいなと思う部分もある。ジーズ入ってからはJS書く機会がたくさんあったため、PHPとJSの連携も前と比較してスムーズにできるようになった気がする。

 

目次

[toc]

 

DM機能におけるDB設計とリレーション

DM機能は、ユーザーに関する情報テーブル(以下Usersテーブル)同士のリレーションとなる。今回は中間テーブルとしてConversationsテーブルを用意し、このDM機能実装に伴うDBリレーションをまとめていく。使うテーブルは3つであり、Users、Conversationsに加え、メッセージ情報を保存するためのMessagesテーブルを用意し、MessagesはUsersとConversationに対してリレーションさせる。

 

【イメージ図】

リレーションイメージ

DM機能の一番のややこしいところは、Usersテーブルをsenderとrecipientの2つに見立ててリレーションを構築する点である。Usersテーブルは、実際のテーブルとしては1つであるが、今回のこのリレーションにおいては、conversationsテーブルを中間テーブルに置く2つのテーブルとみなされる。

 

テーブル設計

Usersテーブル

/database/migrations/xxxx_xx_xx_xxxxxx_create_users_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

 

 

Conversationsテーブル(中間テーブル)

/database/migrations/xxxx_xx_xx_xxxxxx_create_conversations_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateConversationsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('conversations', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('sender_user_id')->index();
            $table->integer('recipient_user_id')->index();
            $table->unique(['sender_user_id', 'recipient_user_id']);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('conversations');
    }
}

 

 

Messagesテーブル

/database/migrations/xxxx_xx_xx_xxxxxx_create_messages_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMessagesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('conversation_id')->index();
            $table->integer('user_id')->index();
            $table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('messages');
    }
}

 

モデル定義

userモデル

app/User.php

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\Pivot;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    // メッセージとの紐付け
    public function messages() {
        return $this->hasMany('App\Message');
    }
}

 

 

conversationモデル

app/conversation.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Auth;

class Conversation extends Model {

    // senderユーザーとの紐付け
    public function senderUser() {
        return $this->hasOne('App\User', 'id', 'sender_user_id');
    }

    // recipientユーザーとの紐付け
    public function recipientUser() {
        return $this->hasOne('App\User', 'id', 'recipient_user_id');
    }

    // DM相手を引っ張るためにotherUserを用意
    public function otherUser() {
        $user_id = Auth::id();
        $other_key = '';
        if ($user_id === $this->sender_user_id) {
            $other_key = 'recipient_user_id';
        } else if ($user_id === $this->recipient_user_id) {
            $other_key = 'sender_user_id';
        }
        return $this->hasOne('App\User', 'id', $other_key);
    }

    // メッセージとの紐付け
    public function messages() {
        return $this->hasMany('App\Message');
    }
    
}

conversationモデルオブジェクトで使える関数として、senderユーザーを引っ張れるものと、recipientユーザーを引っ張ってこれるものをそれぞれ用意。また、conversationはログインユーザーがsenderでもrecipientでもあるため、DM相手を表示するために独自メソッドとしてotherUser()を用意しておくと便利。

 

 

messageモデル

app/message.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Message extends Model {

    // conversationとの紐付け
    public function conversation() {
        return $this->belongsTo('App\Conversation');
    }

    // ユーザーとの紐付け
    public function user() {
        return $this->belongsTo('App\User');
    }
}

 

 

コントローラーの設定

usersコントローラーは省略する。

 

ConversationsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Conversation;
use Validator;
use Auth;

class ConversationsController extends Controller
{
    public function index()
    {
        // ログインユーザーのidがsender_user_id又はrecipient_user_idに含まれるconversationを引っ張ってviewに渡す

        $current_user_id = Auth::user()->id;
        $conversations = Conversation::where('sender_user_id', $current_user_id)->orwhere('recipient_user_id', $current_user_id)->get();
        return view('conversations_index', ['conversations' => $conversations]);
    }

    public function store(Request $request)
    {
        // validationはRequestsで設定
        // sender_user_idとrecipient_user_idが重複していないかで条件分岐
        // 既にconversationが存在するならindexにリダイレクト、そうでなければ新規作成

        $c1 = Conversation::whereRaw('sender_user_id = ? and recipient_user_id = ?', array($request->sender_user_id, $request->recipient_user_id))->get();
        $c2 = Conversation::whereRaw('sender_user_id = ? and recipient_user_id = ?', array($request->recipient_user_id, $request->sender_user_id))->get();

        if (count($c1) === 0 && count($c2) === 0) {
            $conversation = new Conversation;
            $conversation->sender_user_id = $request->sender_user_id;
            $conversation->recipient_user_id = $request->recipient_user_id;
            $conversation->save();
        } else if (count($c1) !== 0) {
            $conversation = $c1[0];
        } else if (count($c2) !== 0) {
            $conversation = $c2[0];
        }
        return redirect()->action(
            'MessagesController@index',
            ['conversation' => $conversation]
        );
    }
}

 

 

MessagesController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests\MessageRequest;
use App\Message;
use App\Conversation;
use Validator;
use Auth;

class MessagesController extends Controller
{
    public function index(Conversation $conversation)
    {   
        // メッセージとconversationをviewに渡す

        $messages = $conversation->messages;
        return view('messages_index', ['messages' => $messages, 'conversation' => $conversation]);
    }

    public function store(MessageRequest $request)
    {
        // validationはRequestsで設定
        // メッセージを保存し、レスポンスとして返す
        // 後ろで載せるmessage.jsにて表示処理を行っている

        $message = new Message;
        $message->content = $request->content;
        $message->conversation_id = $request->conversation_id;
        $message->user_id = $request->user_id;
        $message->save();

        $res = array(
            'message' => $message,
            'user_icon' => $message->user->icon,
            'user_name' => $message->user->name
        );

        return $res;
    }
}

 

 

ビューでの表示

【完成図】

完成図

 

ルーティング

web.php

<?php

use App\Cook;
use Illuminate\Http\Request;

Route::get('/', function () {
    return view('welcome');
});
Auth::routes();

Route::group(['middleware' => 'auth'], function () {
    // users
    Route::get('/users', 'UsersController@index');
    Route::get('/users/show/{users}', 'UsersController@show');
    Route::get('/users/edit/{users}', 'UsersController@edit');
    Route::put('/users/update', 'UsersController@update');
    Route::delete('/user/{user}', 'UsersController@destroy');

    // conversations
    // indexでログインユーザーに紐づいたconversation一覧表示
    // storeでconversation新規作成
    Route::get('/conversations', 'ConversationsController@index');
    Route::post('/conversations/store', 'ConversationsController@store');

    // messages
    // conversationsにネストさせる
    // indexでDM個別ページを表示
    // storeでメッセージ新規作成
    Route::resource(
        'conversations.messages',
        'MessagesController',
        ['only' => ['index', 'store']]
    );

    // home
    Route::get('/home', 'HomeController@index')->name('home');
});

 

 

ログインユーザーに紐づいたconversationの一覧を表示(conversations/index)

ConversationsController.php@indexを叩くためにテキトーにリンクを貼る。

// app.blade.phpなど
<a class="dropdown-item" href="{{ url('conversations') }}">メッセージ</a>

 

一覧が表示されるビューを作成、

conversations_index.blade.php

@extends('layouts.app')

@section('content')
<div class="profile-page sidebar-collapse text-center">
    <h2 class="title">Messages</h2>
    <p class="category">メッセージ履歴</p>
    @if (count($conversations) > 0)
    <div class="container marketing">
        <div class="row">
            @foreach ($conversations as $conversation)
                <div class="col-sm-6 col-lg-4 msgContent">
                    <a href="{{ url('conversations/'.$conversation->id.'/messages') }}" class="">
                        <img src="{{asset('storage/user_icon/'.$conversation->otherUser->icon)}}" class="rounded-circle img-fluid" width="140" height="140">
                        <h3 class="title">{{ $conversation->otherUser->name }}</h3>
                        <p class="description">{{ $conversation->messages[0]->content }}</p>
                    </a>
                </div>
            @endforeach
        </div>
    </div>
    @endif
</div>
@endsection    

 

 

DM個別ページを作成

messages_index.blade.php

@extends('layouts.app')

@section('content')
<div class="profile-page sidebar-collapse text-center">
    <h2 class="title">Message for ...</h2>
    <a href="{{ url('users/show/'.$conversation->otherUser->id) }}" class="userLink">
        <img src="{{asset('storage/user_icon/'.$conversation->otherUser->icon)}}" class="rounded-circle img-fluid" width="80" height="80">
        <p class="category">{{ $conversation->otherUser->name }}</p>
    </a>
    @include('common.errors')
    <div class="col-lg-6 text-center col-md-8 ml-auto mr-auto">
        <div class="form-group">
            <textarea name="content" class="form-control" placeholder="メッセージを入力...">{{ old('content') }}</textarea>
        </div>
        <input type="hidden" name="conversation_id" value="{{ $conversation->id }}" class="conversation_id">
        <input type="hidden" name="sender_id" value="{{ $conversation->senderUser->id }}" class="sender_id">
        <input type="hidden" name="sender_name" value="{{ $conversation->senderUser->name }}" class="sender_name">
        <input type="hidden" name="sender_icon" value="{{ $conversation->senderUser->icon }}" class="sender_icon">
        <input type="hidden" name="recipient_id" value="{{ $conversation->recipientUser->id }}" class="recipient_id">
        <input type="hidden" name="recipient_name" value="{{ $conversation->recipientUser->name }}" class="recipient_name">
        <input type="hidden" name="recipient_icon" value="{{ $conversation->recipientUser->icon }}" class="recipient_icon">
        <input type="hidden" name="messages" value="{{ $messages }}" class="messages">
        <input type="hidden" name="user_id" value="{{ Auth::id() }}" class="user_id">
        <div class="form-group">
            <input type="submit" value="送信" class="btn btn-primary btn-round btn-block btn-lg submit_btn">
        </div>

        <ul class="msgArea"></ul>
    </div>

</div>
@endsection

@section("script")
<script src="{{ asset('js/message.js') }}"></script>
@endsection

 

 

message.jsで非同期表示

js/message.js

async function message() {
    // viewに埋め込んだmessageデータを取得
    const messages = JSON.parse($('.messages').val());

    let content = "";
    messages.forEach(msg => {
        let user_icon = $('.sender_icon').val();
        let user_name = $('.sender_name').val();
        if (msg.user_id == $('.recipient_id').val()) {
            user_icon = $('.recipient_icon').val();
            user_name = $('.recipient_name').val();
        }
        const val = {
            'message': msg,
            'user_icon': user_icon,
            'user_name': user_name
        }
        content += tag(val)
    });

    $('.msgArea').html(content);
}

message();


$('.btn').on('click', function () {
    async function submit() {
        const params = {
            content: $('.form-control').val(),
            conversation_id: $('.conversation_id').val(),
            user_id: $('.user_id').val(),
        }

        await axios.post(`/conversations/${params.conversation_id}/messages`, params)
            .then(res => {
                const content = tag(res.data);
                $('.msgArea').append(content);
                $('.form-control').val('');
            })
            .catch(e => {
                alert(e.response);
            });
    }

    submit();
})

function tag(val) {
    return `<li>
            <img src="/storage/user_icon/${val.user_icon}" alt="icon" class="icon rounded-circle img-fluid">
            <p class="user_name">${val.user_name}</p>
            <p class="content">${val.message.content}</p>
            <p class="date">${val.message.created_at}</p>
            </li>`;
}

 

 

まとめ

最近は梅雨で生乾きの匂いと、Teva履き続けてて自分の足の匂いに悩んでます。臭くないおっさんを目指したい。

以上ありがとうございました。

返信を残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA