【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第1回DB設計とMigration）

# Laravelで始めるTwitter風（Twitterクローン）のSNSツール開発チュートリアル

:::note alert
この記事はLaravel5.7で作成されているため、2024/07 現在 Laravel11で実装するとかなりのエラーが発生します。
プログラムの参考程度にしてください
:::

## 概要

スクールとかの課題だったりLaravelを初めてみたいけど何を作ろうって迷ってる人向けによくあるTwitter風のWEBサイトを作ってみます。

- 第1回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第1回DB設計とMigration）](https://qiita.com/namizatop/items/d79017aa474966244073)
- 第2回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第2回Seeder->ログイン/新規登録）](https://qiita.com/namizatop/items/3c20c0d0565906cc6a1a)
- 第3回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第3回ユーザ関連とフォロー機能）](https://qiita.com/namizatop/items/0c81b0a94a1084cda6de)
- 第4回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第4回ツイートのCRUD機能）](https://qiita.com/namizatop/items/c9ed67f98fc3e5ce67c7)
- 第5回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第5回ツイートのCRUD機能 編集と削除）](https://qiita.com/namizatop/items/2273bd0dcc817785b131)
- 第6回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第6回コメントといいね機能）](https://qiita.com/namizatop/items/0f3f8783c93f3dfa277d)

第1回は環境構築からDB設計->マイグレーション実行までやってみたいと思います。

## 前提

- PHPをある程度理解している
- Laravelが使える環境がある
- MVC構造をある程度理解している

## 環境

- Mac
- Homestead
- Laravel 5.8

## Homestead

今回はHomesteadを使用していますが、DockerでもMAMPでもローカルでも何でもOKです！

## DB設計

DB設計といえど要件がなければ設計のしようが無いので簡潔に要件をまとめます。

- ユーザがログインできる(ログイン状態で無いと閲覧や投稿は出来ない)
- ユーザ毎にツイート（記事）を投稿できる
- ツイートに対してコメントといいねが出来る
- コメントにはいいねとコメントはできない
- ユーザ同士でフォローができ、自身のタイムラインにはフォローしているユーザのツイート以外見えない

とまぁこんな感じでしょうか。実際のTwitterはログインしなくても見えるのですが、
~~めんどくさいので~~今回はログイン状態時しか閲覧できないとします。

テーブルはざっとこんな感じでしょうか。

- usersテーブル
 - ユーザを管理するテーブル
- tweetsテーブル
 - ユーザ毎のツイートを管理するテーブル
- commentsテーブル
 - ツイートに対してコメントする機能
- favoritesテーブル
 - ツイートに対するいいね機能
- followersテーブル
 - フォロー関係を管理するテーブル 

画像にするとこんな感じです。

<img width="1180" alt="スクリーンショット 2019-08-13 13.19.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293438/9e7d31f8-559a-5294-237c-997ba497dc5b.png">


テーブルの説明はこの記事が分かりやすいと思います。（Rubyですが）
[【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】🎸](https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E6%A9%9F%E8%83%BD%E3%81%A8%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E6%A9%9F%E8%83%BD%E3%82%92er%E5%9B%B3%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E8%A8%AD%E8%A8%88%E3%81%97%E3%82%88%E3%81%86)

## Migration(マイグレーション)

Migrationとは言うならばテーブルの設計図のようなものです。
テーブルの構築やカラムの追加などの更新をチームで共有しやすいようにすることが出来ます。
これについては良記事が沢山あるので詳しくは書きません。

### artisanコマンド

Laravelには`artisan`コマンドというターミナルで使える便利なコマンドが用意されています。
例えば以下のコマンドを入力すると`app/Http/Controllers`の中に`SampleContorller`というファイルが生成されます。

```terminal
php artisan make:controller SampleController
```

この要領でMigrationとModelを作っていきたいと思います。

### Migrationファイルの作成

> make:model 〇〇 -m とすることでModelとMigrationを同時に作成してくれます。
それと分かりやすいように`app/Models`と言うディレクトリを作成してそこにまとめて管理します。

`Tweets`テーブル

```terminal
php artisan make:model Models/Tweet -m
```

`Comments`テーブル

```terminal
php artisan make:model Models/Comment -m
```

`Favorites`テーブル

```terminal
php artisan make:model Models/Favorite -m
```

`Followers`テーブル

```terminal
php artisan make:model Models/Follower -m
```

### Migrationを実際に書いていく

ユーザに関しては最初から存在している`2014_10_12_000000_create_users_table.php`と言うファイルが用意されているのでそちらにカラムを追加します。

#### `Users`

```php:2014_10_12_000000_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('screen_name')->unique()->null()->comment('アカウント名');
            $table->string('name')->null()->comment('ユーザ名');
            $table->string('profile_image')->nullable()->comment('プロフィール画像');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

#### `Tweets`

```php:2019_08_11_082006_create_tweets_table.php
<?php

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

class CreateTweetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id')->comment('ユーザID');
            $table->string('text')->comment('本文');
            $table->softDeletes();
            $table->timestamps();

            $table->index('id');
            $table->index('user_id');
            $table->index('text');

            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade')
                ->onUpdate('cascade');
        });
    }

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

この部分で`Users`テーブルと外部キー接続を宣言しています。

```php:2019_08_11_082006_create_tweets_table.php
            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade')
                ->onUpdate('cascade');
```


#### `Comments`

```php:2019_08_11_084403_create_comments_table.php
<?php

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

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id')->comment('ユーザID');
            $table->unsignedInteger('tweet_id')->comment('ツイートID');
            $table->string('text')->comment('本文');
            $table->softDeletes();
            $table->timestamps();

            $table->index('id');
            $table->index('user_id');
            $table->index('tweet_id');

            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade')
                ->onUpdate('cascade');

            $table->foreign('tweet_id')
                ->references('id')
                ->on('tweets')
                ->onDelete('cascade')
                ->onUpdate('cascade');
        });
    }

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

#### `Favorites`

```php:2019_08_11_082124_create_favorites_table.php
<?php

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

class CreateFavoritesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('favorites', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id')->comment('ユーザID');
            $table->unsignedInteger('tweet_id')->comment('ツイートID');

            $table->index('id');
            $table->index('user_id');
            $table->index('tweet_id');

            $table->unique([
                'user_id',
                'tweet_id'
            ]);

            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade')
                ->onUpdate('cascade');

            $table->foreign('tweet_id')
                ->references('id')
                ->on('tweets')
                ->onDelete('cascade')
                ->onUpdate('cascade');
        });
    }

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

#### `Followers`テーブル

ここ少しややこしいですね。簡単に言うと自分がフォローしているユーザのツイートをTLに表示するときは
自分が`following_id `で相手（自分がフォローしているユーザ）が`followed_id`になります。

```php:2019_08_11_082050_create_followers_table.php
<?php

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

class CreateFollowersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('followers', function (Blueprint $table) {
            $table->unsignedInteger('following_id')->comment('フォローしているユーザID');
            $table->unsignedInteger('followed_id')->comment('フォローされているユーザID');
            
            $table->index('following_id');
            $table->index('followed_id');

            $table->unique([
                'following_id',
                'followed_id'
            ]);
        });
    }

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

```

**uniqueを登録する事で以下のキーの組み合わせで同じIDの登録を防ぐことができる**

```php
$table->unique([
    'following_id',
    'followed_id'
]);
```

## Model

### Modelに設定を記述する

LaravelではEroquentを使用して登録/編集する時にデフォルトでTimestampが登録するようになっていたり、
登録/更新を許可するカラムを指定したりと様々な設定が可能です。

#### `Users`

`screen_name`と`profile_image`を追加したので、登録/更新を許可するために
**$fillable**の配列にカラムを指定します。

**※Userは元々`app`直下にあるので`app/Models`に移動してください。その際namespaceを変更するのを忘れずに！**

```php:app/Models/User
<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

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

#### `Tweets`

TweetテーブルではSoftDeleteという論理削除（削除してもDBには残るがシステム上削除したとみなす機能）を使える様に設定します。
ついでに登録/更新を許可するために`$fillable`は`text`カラムだけ許可しておきます。

> $fillableはLaravelで用意されているメンバ変数です。
$fillableにカラム名を定義するとそれ以外のカラムを登録/更新でエラーを吐きます。
つまりホワイトリストですね。
逆に$guardedというのはブラックリストで登録/更新できないカラムを指定します。
基本的にはどちらでも可です。

```php:app/Models/Tweet.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\softDeletes;

class Tweet extends Model
{
    use SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'text'
    ];
}
```

#### `Comments`

こちらも`Tweet`と同じです。

```php:app/Models/Comment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\softDeletes;

class Comment extends Model
{
    use SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'text'
    ];
}
```

#### `Favorites`

デフォルトでTimestampが設定されているのでfalseにします。

```php:app/Models/Favorite.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Favorite extends Model
{
    public $timestamps = false;
}
```

#### `Followers`

Followerテーブルはincrementも使用しないという設定とprimary_keyを指定する設定を合わせて記述します。

```php:app/Models/Follower.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Follower extends Model
{
    protected $primaryKey = [
        'following_id',
        'followed_id'
    ];
    protected $fillable = [
        'following_id',
        'followed_id'
    ];
    public $timestamps = false;
    public $incrementing = false;
}
```

### リレーションの親子関係

#### `Users`

```php:app/Models/User.php
    // 省略

    public function followers()
    {
        return $this->belongsToMany(self::class, 'followers', 'followed_id', 'following_id');
    }

    public function follows()
    {
        return $this->belongsToMany(self::class, 'followers', 'following_id', 'followed_id');
    }
```

#### `Tweets`

```php:app/Models/Tweet.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\softDeletes;

class Tweet extends Model
{
    use SoftDeletes;

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

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function favorites()
    {
        return $this->hasMany(Favorite::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
```

#### `Comments`

```php:app/Models/Comment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\softDeletes;

class Comment extends Model
{
    use SoftDeletes;

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

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
```

### Migration実行！

```terminal
php artisan migrate
```

これでDBは用意できました！第1回はとりあえずここまで！

次回 -> [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第2回Seeder->ログイン/新規登録）](https://qiita.com/namizatop/items/3c20c0d0565906cc6a1a)