Laravelでかんたんな日記を作る

完全に自分の学習日記ですが、チュートリアル的なものになればいいなあ、ということで随時まとめます。

# 環境構築
最新のlaravel7を用います。
ここではwindows 10 pro+vscode+laradockでの開発を想定しています。
そのへんのややこしいところは他の記事を参照してください。

* [Laradockを使ったLaravel開発環境構築](https://qiita.com/J_Shell/items/695a30fd38444d065ae5)
* [自分の記事](https://qiita.com/kefian1go/items/bbc8f9d51fe890194ad7)

各自laradockをクローンして、`docker-compose up -d nginx mysql`を実行し、`composer　laravel/laravel hogehoge`してください。

# テンプレートの作成
[【Laravel】Viewのbladeの書き方 ](https://qiita.com/sola-msr/items/a128f4e0900f3541690c)をベースに（そのまま丸パクリ)してるので割愛。

#データベース・テーブルの作成
日記なので記事を保存しなければなりません。そのためにかんたんなDBを作成します。
今回はこんな感じの1つのテーブルがあれば良いでしょう。
**articlesテーブル**

|id|タイトル|本文|投稿日|投稿者|編集日|編集者|
|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|
|id|title|content|create_at|created_by|update_at|update_by|
|bigint|char(128)|text|timestamp|tinyint(4)|timestamp|tinyint(4)|

ここで投稿者と編集者をtinyintにしているのは、今後会員機能を追加するためです。今回は割愛。

mysqlに接続し、データベースを作成します。DB名は標準の`laravel`とします。

やり方は割愛。

##マイグレーションファイルの作成
```sh
$ artisan make:migration articles
Created Migration: 2020_03_15_090146_articles
```
中身を以下のように編集します。

```2020_03_15_090146_articles.php
<?php

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

class Articles extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function(Blueprint $table){
            $table->id();
            $table->char('title', 128);
            $table->text('content');
            $table->timestamps();
            $table->tinyInteger('created_by');
            $table->tinyInteger('update_by');


        })
    }

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

```

マイグレーションを実行します。

```shell
$ artisan migrate
Migrating: 2020_03_15_090146_articles
Migrated:  2020_03_15_090146_articles (0.04 seconds)
```
テーブルが作成されたことを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392324/dfb97eca-f5f1-985a-a062-e46c7c1293d5.png)

#投稿画面の作成
日記を投稿する画面を作ります。

###Eloquentモデルの作成
ここではORMを利用します。まずArticleモデルを作ります。
ORMについては[私の記事](https://qiita.com/kefian1go/items/2f5f950bf36e7ad58cf8)を参考にしてください。

```shell
$ artisan make:model Article
Model created successfully.
```

Articleモデルを以下のように編集します。

```Article.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected $table = 'articles';
    // ユーザー系は作っていないのでデフォルト値を指定する
    protected $attributes = [
        'created_by' => 0,
        'update_by' => 0,
    ];
}


```

##投稿画面の作成
new_article.blade.phpを作成します。今回は投稿さえできればいいのでデザインは考慮しません。

```html
@extends('layouts.app')

@section('title','記事の作成')

@section('content')
<div class="content">
<form method="post">
    @csrf
    <label>タイトル： <input id="title" type="text" name="title" value=""></label>
    <label>本文： <input id="content" type="text" name="content" value=""></label>
    <input type="submit" name="submit" value="投稿する">
</form>
</div>

@endsection
```
ちなみにlayouts.appは丸パクリしたものなので割愛します。

では、次に投稿の処理を行うコントローラークラスを作成します。

```shell
$ artisan make:controller PostController
Controller created successfully.
```
編集します。

```PostController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Article;

class PostController extends Controller
{
    public function create(Request $request){
        $article = new Article();

        $article->title = $request->title;
        $article->content = $request->content;

        $article->save();
        return redirect('/'); //とりあえずトップページに戻る。
    }
}
```

###ルーティングの設定

ルーティングの設定をしてDBに保存させます。

```web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('index');
});

Route::get('/create', function () {
    return view('new_article');
});

Route::post('/create', 'PostController@create');

```

#投稿結果を表示させる
日記なので読めなければ意味がありません。
ここではとりあえずindexに一覧表示させるようにします。
コントローラークラスを作成します。

```shell
$ artisan make:controller GetController
Controller created successfully.
```
編集します。

```GetController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Article;

class GetController extends Controller
{
    public function read(){
        $articles = Article::all();

        return view('index', ['Articles' => $articles]);
    }
}

```
##ルーティングの設定
以下のように書き換えます。

```web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('index');
});

Route::get('/', 'GetController@read');

Route::get('/create', function(){
    return view('new_article');
});

Route::post('/create', 'PostController@create');

```

##bladeの編集
ようやく最後です。bladeを編集します。

```index.blade.php
@extends('layouts.app')

@section('title', 'laravel日記')

@section('content')
    <div class="link"><a href='/create'>新規投稿<a></div>
    <div class="content">投稿内容</div>
    @foreach($articles as $article)
    <p>
    
    投稿番号:{{$article->id}} /<b>{{$article->title}}</b><br>
    {{$article->content}}<br>
    投稿日:{{$article->created_at}}
    </p>
    @endforeach
@endsection
```

これでとりあえず動きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392324/22c9c31f-203d-b9e4-5999-af02772a1c9f.png)


#編集機能
今のままでは一度書いた日記を編集することができません。そこで編集機能を作りましょう。
編集するためのコントローラクラスを作成します。

```UpdateController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Article;

class UpdateController extends Controller
{
    public function update(Request $request, $id){
        $article = Article::find($id);
        $article->title = $request->input('title');
        $article->content = $request->input('content');
        $article->save();
        return redirect(('/'));
    }
}
```
1つの記事を呼び出す関数をgetcontrollerに作ります。

```GetController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Article;

class GetController extends Controller
{
    public function read(){
        $articles = Article::all();

        return view('index',['articles' => $articles]);
    }

    public function show($id){
        $article = Article::find($id);

        return view('show_article',['article' => $article]);
    }
}

```

Routeを以下のように追記します。

```web.php
Route::get('article/{id}','GetController@show')->where('id','[1-9]+');

Route::post('article/{id}', 'UpdateController@update')->where('id','[1-9]+');
```

そして編集画面を作ります。

```show_article.blade.php
@extends('layouts.app')

@section('title','記事の更新')

@section('content')
<div class="content">
<form method="post" >
    @csrf
    <lavel>日付: {{$article->created_at}}</lavel><br>
    <label>タイトル： <input id="title" type="text" name="title" value="{{$article->title}}"></label>
    <label>本文： <input id="content" type="textarea" name="content" value="{{$article->content}}"></label>
    <input type="submit" name="submit" value="更新する">
</form>
</div>

@endsection
```

これで編集機能は出来ました。

# 削除機能
続いて削除機能を追加します。

```DeleteController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Article;

class DeleteController extends Controller
{
    public function delete(int $id){
        Article::find($id)-> delete();

        return redirect('/');
    }
}
```
ルートに以下を追記

```web.php
Route::get('article/{id}/delete', 'DeleteController@delete')->where('id','[1-9]+');
```
また、bladeにも以下を追記します。

```show_article.blade.php
<!-- 記事の削除 -->
<a href="/article/{{$article->id}}/delete">削除</a>


```
これで一通りのCRUDは完成しました！

#paginateを実装する
現在のコードだと延々と新しいものが下に追加されます。
しかし、実際は

* 昇順、降順変えられるようにしたい
* ページネイトさせたい

ということが出てきますよね。
そこでpaginateをちゃちゃっと実装します。

##コントローラーを書き換える
全体を表示するコントローラーのメソッドを以下のように変更します。

```GetController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Article;

class GetController extends Controller
{
    public function read(){
        $articles = Article::paginate(10); //10件表示

        return view('index',['articles' => $articles]);
    }

    public function show($id){
        $article = Article::find($id);

        return view('show_article',['article' => $article]);
    }
}
```

そして全件表示していたbladeに**一行だけ追記**します。
そんなにかんたんなの？
**かんたんなんです！**

```index.blade.php

{{$articles->links()}}
```

結果、このようなページネーションが出来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392324/9c82b1ae-00be-0713-bdcb-7910bcc3c0fb.png)

なんだか不格好！でもとりあえず動く！

#認証機能を作る
今回は日記を作る予定でしたので続いて認証機能を作ります。
Laravelならかんたんに作れます。

## ルート定義
この辺は、[laravelドキュメント](https://readouble.com/laravel/7.x/ja/authentication.html)まんまです。
以下2つのコマンドを実行しましょう。

```shell
composer require laravel/ui
```

```shell
php artisan ui vue --auth
```

これらの処理が完了したらブラウザでlocalhostにアクセスしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392324/3d702a58-0e9c-218c-d251-3fa8a1313c31.png)

こんな感じになっていると思います。いい加減bootstrapを読み込ませないといけないようですね。

# 脱線：bootstrapの設定
laravel6からはデフォルトではbootstrapがインストールされなくなりました。
そこでbootstrapをインストールしましょう。
laravel/uiはすでにインストールしてあるので、その続きで、以下のコマンドを実行します。

```shell
php artisan ui bootstrap
```
続いて以下を実行します。

```shell
npm install && npm run dev
```
すると以下のような画面になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392324/0f84a627-ff60-3800-5a64-7145bfbac0b2.png)
paginateもしっかり機能しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392324/57dfdbb4-50fa-ccfb-905c-5cc4b10dff58.png)

#もとにもどって：ログインした人だけが書き込めるようにしよう
脱線もこの辺にしておいて・・・
日記ですから、ログインした人のみが書き込めるようにしましょう。
とりあえず、以下のページをログイン必須にします。

* 新規投稿ページ
* 編集ページ
* 削除機能

ルートの編集を行います。
以下の文字列をrouteの最後尾に追加するだけでOKです。

```php
->middleware('auth');
```

つまり

```web.php
<?php

use App\Http\Controllers\GetController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('index');
});

Route::get('/', 'GetController@read');

Route::get('/create', function(){
    return view('new_article');
})->middleware('auth');;

Route::post('/create', 'PostController@create')->middleware('auth');;

Route::get('article/{id}', 'GetController@show')->where('id','[1-9]+')->middleware('auth');

Route::post('article/{id}', 'UpdateController@update')->where('id','[1-9]+')->middleware('auth');

Route::get('article/{id}/delete', 'DeleteController@delete')->where('id','[1-9]+')->middleware('auth');
Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');
```
これで完了です。

ログインしていない場合以下のような画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392324/5929dd0c-2ac6-77c5-dfd6-8065c2c398a5.png)

# 特定ユーザーのみ投稿・編集可能にしよう
現在のままだとログインユーザーを作れば誰でも投稿、編集が可能になってしまいます。
そこで、特定のユーザーのみ投稿編集が可能な状態にしたいと思います。
Laravelではpolicyというものを使い、この認可機能を実装することができます。

ポリシーを作るために、以下のコマンドを実行します。

```shell
php artisan make:policy ArticlePolicy --model=Article
```
すると以下のようなファイルが生成されます。

```ArticlePolicy.php
<?php

namespace App\Policies;

use App\Article;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class ArticlePolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view any articles.
     *
     * @param  \App\User  $user
     * @return mixed
     */
    public function viewAny(User $user)
    {
        //
    }

    /**
     * Determine whether the user can view the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function view(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can create articles.
     *
     * @param  \App\User  $user
     * @return mixed
     */
    public function create(User $user)
    {
        //
    }

    /**
     * Determine whether the user can update the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function update(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can delete the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function delete(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can restore the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function restore(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can permanently delete the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function forceDelete(User $user, Article $article)
    {
        //
    }
}
```
このファイルに記述していけばOKです。

## ポリシーの登録
その前にポリシーを登録しなければいけません。
この作業をすることで、どのポリシーを使って認可するかを指定することが出来ます。

```AuthServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
        Article::class => ArticlePolicy::class,
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        //
    }
}
```

## ポリシーの記述
それではポリシーを記述していきましょう。
本来ならば投稿者は自分の投稿した記事のみ削除できるようにすべきです。しかし、今回の場合、投稿者idは0に固定しています。
そのため、ユーザーIDが1の場合のみ投稿や編集ができるようにしてみましょう。

```ArticlePolicy.php
<?php

namespace App\Policies;

use App\Article;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class ArticlePolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view any articles.
     *
     * @param  \App\User  $user
     * @return mixed
     */
    public function viewAny(User $user)
    {
        //
    }

    /**
     * Determine whether the user can view the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function view(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can create articles.
     *
     * @param  \App\User  $user
     * @return mixed
     */
    public function create(User $user)
    {
        return $user->id === 1;
    }

    /**
     * Determine whether the user can update the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function update(User $user, Article $article)
    {
        return $user->id === 1;
    }

    /**
     * Determine whether the user can delete the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function delete(User $user, Article $article)
    {
        return $user->id === 1;
    }

    /**
     * Determine whether the user can restore the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function restore(User $user, Article $article)
    {
        return $user->id === 1;
    }

    /**
     * Determine whether the user can permanently delete the article.
     *
     * @param  \App\User  $user
     * @param  \App\Article  $article
     * @return mixed
     */
    public function forceDelete(User $user, Article $article)
    {
        return $user->id === 1;
    }
}

```

そして最後にそれぞれのコントローラーのメソッドに以下を追加します。

```php
 $this->authorize('create', Article::class); // postメソッドのみ
$this->authorize('update', $article); //そのほかのメソッド

```
はい！認可もできました！

#まとめ
一応これで一通りの機能は完成しました。
みなさん、お疲れさまでした。
個人的には認証認可の部分にハマったのでその部分は別にまとめられたら、と思います。