Laravelで投稿アプリの機能を作成 ~CRUD編~

# はじめに
Laravelで汎用的に開発を進められるようにするべく、
チュートリアル形式で記事を作成したのでご参考になれば幸いです。

【目的】
・汎用的に使えるレシピを提供 → Laravelでの開発効率アップを目指す

# こんな方におすすめ
・Laravelを書籍などで一通り勉強された方

#前提条件
・RESTful APIを利用したCRUD機能 ※分からない方はLaravelの公式ドキュメント参照
・VirtualBoxとVagrantで開発
・Vueは今回はなし
・認証はひとまずなし（記事テーブルに外部キーのフィールドのみ持たせておく）

# 完成イメージ
【実装した機能】
・記事の投稿　・記事の描画　・記事の削除　・詳細ページの描画
・[追加]記事の編集

所要時間：30分ほど
※すでにLaravelプロジェクトを構築した方に限る
※初回はいろいろインストール等に時間がかかります

# Laravelプロジェクト構築
下記からさくっと環境構築してください
すでにされたことがある方なら10分くらいでできるはずです
Mac用の記事ですが、Windowsでも基本は同じです
https://qiita.com/7968/items/68b3566d92d2b007038e

# 記事テーブル定義
postsテーブル
※テーブル名を任意に変更した場合、それ以降名前を変更してください
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| user_id    | int(11)          | NO   |     | NULL    |                |
| title      | varchar(255)     | NO   |     | NULL    |                |
| message    | varchar(255)     | NO   |     | NULL    |                |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

# 手順レシピ

1.マイグレーションファイル生成〜編集〜マイグレート
マイグレーションファイル生成
` $ php artisan make:migration create_posts_table `

```php:/database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php
// 中略
public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');

            //追加したいデータ型とカラムを追加
            $table->integer('user_id');
            $table->string('title')->nullable();
            $table->string('message')->nullable();

            $table->timestamps();
        });
    }
// 中略
```
マイグレート
` $ php artisan migrate `


2.モデル作成
` $ php artisan make:model Post `
※モデル名はキャメル記法の単数形

```php:/app/Post.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{   
    // 初期設定を定義する
    protected $table = 'posts';
    protected $guarded = array('id');

    // 以下独自でいろいろ定義可能
    // public function getData()
    // {
    //     return $this->id. ": this is :" . $this->message;
    // }
}

```
「$guarded」に関してまとめられた記事です
https://qiita.com/kk_take/items/3e0639ed605f74c34619

3.コントローラー作成
` $ php artisan make:controller PostController --resource `
「--resource」をつけることでRESTfulなデータになります

```php:/Http/Controllers/PostController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

// Eloquentを使うので必ず入れてください
use App\Post;

// Validatorを使うのに必要
use Validator;

class PostController extends Controller
{
    public function index()
    {
        $items = Post::all();
        return view('post.index', ['items' => $items]); // ビューの描画
        // return $items->toArray(); // JSONデータで描画
    }

    // 描画はindexでしているので、不要
    // public function create()
    // {
    //     return view('post.create');
    // }

    public function store(Request $request)
    {
        $post = new Post;
        $form = $request->all();

        // 最低限なバリデーション処理です。ここでは特に説明はしません。
        $rules = [
            'user_id' => 'integer|required', // 2項目以上条件がある場合は「 | 」を挟む
            'title' => 'required',
            'message' => 'required',
        ];
        $message = [
            'user_id.integer' => 'System Error',
            'user_id.required' => 'System Error',
            'title.required'=> 'タイトルが入力されていません',
            'message.required'=> 'メッセージが入力されていません'
        ];
        $validator = Validator::make($form, $rules, $message);

        if($validator->fails()){
            return redirect('/post')
                ->withErrors($validator)
                ->withInput();
        }else{
            unset($form['_token']);
            $post->user_id = $request->user_id;
            $post->title = $request->title;
            $post->message = $request->message;
            $post->save();
            return redirect('/post');
        }
    }

    public function show($id)
    {
        $item = Post::find($id);
        return view('post.show', ['item' => $item]);
    }

    // 描画はshowでしているので不要
    // public function edit($id)
    // {
    // }

    // public function update(Request $request, $id)
    // {
        // UPDATE処理をビュー含めて下記に追加しました。
    // }

    public function destroy($id)
    {
        $items = Post::find($id)->delete();
        return redirect('/post');
    }
}
```

RESTfulなチートシートはこちらを参照
https://qiita.com/fagai/items/a1bf55b6249aee03a624

4.ルーティング設定
ルーティングを設定します。この1行で自動でCRUDに対応するルーティングを設定してくれます。
非常に便利でコードも見通しのいいものになります。

```php:/routes/web.php
// RESTfulサービスのルーティング
Route::resource('/post', 'PostController');

// RESTfulサービス省略なしだとこうなる
// Route::get('post', 'PostController@index')->name('post.index'); // 一覧
// Route::post('post', 'PostController@store')->name('post.store'); // 保存
// Route::get('post/create', 'PostController@create')->name('post.create'); // 作成
// Route::get('post/{post_id}', 'PostController@show')->name('post.show'); // 表示
// Route::get('post/edit/{post_id}', 'PostController@edit')->name('post.edit'); // 編集
// Route::put('post/{post_id}', 'PostController@update')->name('post.update'); // 更新
// Route::delete('post/{post_id}', 'PostController@destroy')->name('post.destroy'); // 削除
```

5.ビューを作成する
まずはベースのBladeテンプレート
※一応Bootstrap入れておきます

```html:/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}"></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
    <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <link href="{{ asset('css/style.css') }}" rel="stylesheet">
    
    <link rel="stylesheet" type="text/css" href="{{ asset('css/dropify.css') }}">
    <script src="{{ asset('js/dropify.js') }}"></script>
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
            <div class="container">
                <a class="navbar-brand" href="{{ url('/') }}">
                    {{ config('app.name', 'Laravel') }}
                </a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav mr-auto">

                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="navbar-nav ml-auto">
                        <!-- Authentication Links -->
                        @guest
                        <li class="nav-item">    
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('user.index') }}">User</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('post.index') }}">Post</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('login') }}">Login</a>
                            </li>
                            @if (Route::has('register'))
                                <li class="nav-item">
                                    <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
                                </li>
                            @endif
                        @else
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('user.index') }}">User</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('post.index') }}">Post</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('login') }}">Login</a>
                            </li>
                            <li class="nav-item dropdown">
                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>

                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="{{ route('logout') }}"
                                       onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                        {{ __('Logout') }}
                                    </a>

                                    <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                        @csrf
                                    </form>
                                </div>
                            </li>
                        @endguest
                    </ul>
                </div>
            </div>
        </nav>

        <main class="py-4">
            @yield('content')
        </main>
    </div>
<script src="{{ asset('js/jquery.js') }}"></script>
</body>
</html>
```

次にTopページ

```html:/views/post/index.blade.php
@extends('layouts.app')
@section('title', '投稿アプリ')

@section('content')
    @section('maincopy', '投稿してください')

    <!-- 投稿フォーム -->
    <form action="/post" method="post">
        {{ csrf_field() }}
        
        <!-- value仮入れ(Userモデルとリレーションするのに必要) -->
        <input type="hidden" name="user_id" value="1">
        @if($errors->has('title'))
            <div class="error_msg">{{ $errors->first('title') }}</div>
        @endif
        <input type="text" class="form" name="title" placeholder="タイトル" value="{{ old('title') }}">

        @if($errors->has('message'))
            <div class="error_msg">{{ $errors->first('message') }}</div>
        @endif
        <div>
            <textarea class="form" name="message" placeholder="メッセージ">{{ old('message') }}</textarea>
        </div>
        <input type="submit" class="create" value="投  稿">
    </form>

    <!-- 記事描画部分 -->
    @if(count($items) > 0)
        @foreach($items as $item)
            <div class="alert alert-primary" role="alert">
                <a href="/post/{{ $item->id }}" class="alert-link">{{ $item->title }}</a>
                <form action="/post/{{ $item->id }}" method="POST">
                {{ csrf_field() }}
                <input type="hidden" name="_method" value="DELETE">
                <input type="submit" class="delete" value="削除">
                </form>
            </div>
        @endforeach
    @else
        <div>投稿記事がありません</div>
    @endif
@endsection
```

最後に記事詳細ページ

```html:/views/post/show.blade.php
@extends('layouts.app')
@section('title', '詳細記事')

@section('content')
    @section('maincopy', '詳細記事')

    @if($item !== '')
        <div class="headcopy">Title</div><hr>
        <div class="text">{{ $item->title }}</div>

        <div class="headcopy">Message</div><hr>
        <div class="text">{{ $item->message }}</div>
    @endif

    <a href="/post"><img src="{{ asset('img/edit.svg') }}" class="add" alt="topへ"></a>
@endsection
```

src="{{ asset('img/edit.svg') }}"の部分、
publicフォルダに「img」フォルダをつくり、そこにアイコン画像を入れてください
※UIはこちらのサイトから無料でダウンロードすることができます
https://www.ikonate.com/

6.スタイルを調整する

```css:/public/css/post.css
body {
    width: 100%;
    background-color: #fff;
}

hr {
    margin: 0;
    padding: 0;
}

.header {
    color: #fff;
    text-align: center;
    background: #6495ed;
    margin: 0 0 20px 0;
    padding: 7vh;
}

.footer {
    font-size: 12px;
    color: #4169e1;
    text-align: center;
    margin: 50px 0 0 0;
}

h1 {
    color: #fff;
    font-size: 20px;
    font-weight: normal;
}

h3 {
    color: #4169e1;
    font-size: 16px;
    font-weight: normal;
}

.error_msg {
    color: #cd5c5c;
    font-size: 12px;
    margin: 10px 0 0 0;
}

.form {
    color: #4169e1;
    font-size: 12px;
    width: 100%;
    margin: 10px auto 10px;
    padding: 10px;
    border: none;
    outline: none;
    border-radius: 10px;
    box-sizing: border-box;
    box-shadow: 0 0 3px rgba(65,105,255,0.4);
}

.form:last-child {
    margin-bottom: 20px;
}

.form:focus {
    box-shadow: 0 0 7px #4169e1;
}

input[type=text] {
    height: 35px;
}

textarea {
    height: 50px;
    transition: 1s;
}

textarea:focus {
    height: 150px;
}

input[class=create] {
    width: 100%;
    font-size: 15px;
    font-weight: 100;
    color: #fff;
    margin: 0 auto 50px;
    padding: 5px 0 5px 0;
    background: #6495ed;
    border:1px solid #4169e1;
    border-radius: 5px;
    box-sizing: border-box;
    transition: 1s;
}

input[class=create]:hover {
    box-shadow: 0 0 7px #4169e1;
    opacity: .7;
}

input[class=delete] {
    font-size: 13px;
    font-weight: 100;
    color: #4169e1;
    margin: 5px 0 0 0;
    padding: 0;
    background: none;
    border: none;
}

img.add {
    width: 35px;
    background: #fff;
    padding: 5px;
    box-sizing: border-box;
    border-radius: 50%;
    position: absolute;
    top: 5px;
    right: 5px;
    transition: .5s;
}

img.add:hover {
    box-shadow: 0 0 7px #4169e1;
    opacity: .7;
}

.headcopy {
    color: #ccc;
    font-size: 15px;
    font-weight: bold;
    margin: 0 0 -6px 0;
}

.text {
    font-size: 15px;
    margin: 10px 0 20px 0;
}
```

CSSもシンプルにするため、モバイル版CSSのみにしています。
おのおので、良い感じのブレイクポイントのところでレスポンシブにしていただければと思います。

` 192.168.10.10/post `でアクセス

# 機能追加
記事のUPDATE

```php:/Http/Controllers/PostController.php
// 中略
    public function update(Request $request, $id)
    {
        $post = Post::find($id);
        $form = $request->all();
        
        // (注)バリデーションするかテーブル側をnull許容すること

        unset($form['_token']);
        $post->user_id = $request->user_id;
        $post->title = $request->title;
        $post->message = $request->message;
        $post->save();
        return redirect('/post');
    }
// 中略
```

```html:/views/post/show.blade.php
<!--中略-->
@if($item !== '')
        <div class="headcopy">Title</div><hr>
        <div class="text">{{ $item->title }}</div>

        <div class="headcopy">Message</div><hr>
        <div class="text">{{ $item->message }}</div>

        <!--ここから追加-->
        <form action="/post/{{$item->id}}" method="POST">
            {{ csrf_field() }}           
            <!-- value仮入れ(Userモデルとリレーションするのに必要) -->
            <input type="hidden" name="user_id" value="1">
            <input type="text" class="form" name="title" placeholder="タイトル" value="{{ $item->title }}">
            <div>
                <textarea class="form" name="message" placeholder="メッセージ">{{ $item->message }}</textarea>
            </div>
            <input type="hidden" name="_method" value="PUT">
            <input type="submit" class="create" value="変　更">
        </form>
        <!--ここまで追加-->
    @endif
<!--中略-->
```


# あとがき
慣れればこの最低限の記事投稿機能を1時間もあれば実装できるかと思います。
この他にもUserモデルとのリレーション、画像投稿など出来次第、別ページを作成し、更新していきたいと思います。その際はこのページにURLリンクをつけますので、そちらからジャンプお願いします。もし、うまくいかない・分からない部分や、ここはこうした方が良いよと部分がございましたら、何なりとご意見お願いします。


Laravelで投稿アプリの機能を作成
~CRUD編~
https://qiita.com/ProgramingDai/items/cf6944f9cd0ac08f4e3e
~リレーション編~
https://qiita.com/ProgramingDai/items/249acc8894079ee58268
~ログインカスタマイズ編Ⅰ~
https://qiita.com/ProgramingDai/items/fee669e5a8cf67f0e38e
~ログインカスタマイズ編Ⅱ~
https://qiita.com/ProgramingDai/items/4fe2e3cc90987356c9c4

# 参考資料
参考書籍： PHPフレームワーク Laravel入門
https://blog.hiroyuki90.com/articles/laravel-books/
公式ドキュメント： https://readouble.com/laravel/
環境構築： https://qiita.com/7968/items/68b3566d92d2b007038e
RESTfulに関して: https://qiita.com/fagai/items/a1bf55b6249aee03a624
guardedに関して: https://qiita.com/kk_take/items/3e0639ed605f74c34619
UIアイコン提供サイト： https://www.ikonate.com/