Laravelでwiki的なものをつくってみる(後編)

Schooアドベントカレンダーの**21**日目の記事です。

[前編はこちら](http://qiita.com/ohida/items/ff2e705c8098d82dfc73)

## 前編のまとめ
- Schooの@ohidaです
- 2016年9月Schooにジョインしたてへぺろ系非エンジニア職
- 好きなアニメは「斉木楠雄のΨ難」
- Laravelでwiki的なものをつくろう
- 色々あったけれど、ページの一覧画面が表示されるところまでできた

というわけで前回のつづきを楽しくつくっていきましょう。

## パーマリンク

リンクをはってしまってもう我慢できないと思いますので、クリックしてみましょう。真っ白な画面が表示されたら成功の証です。（正しく動作していない場合はエラー画面になります）

ちなみにURLはこんな感じになっていると思います。

```
http://wiki.dev/pages/blanditiis
```

コントローラの該当のアクションをみてみます。ルーティングテーブルを確認してみましょう。

| Verb | URI | Action | Route Name |
|:--|:--|:--|:--|
| GET | /pages/{page} | show | pages.show |

`PostController`の`show()`メソッドが呼ばれているようです。
Laravelがコントローラ作成時にCRUDに対応するメソッドの雛形をつくってくれていますが、当然ですがまだからっぽです。

```php:app/Http/Controllers/PageController.php
public function show($id)
{
    //
}
```

ここで指定されたページを表示するためのロジックを書きたいところですが、ちょっとまってください、`$id`というのが渡ってきていますね。これはルーティング時にデフォルトではIDが渡されることを想定しているということで、実態にあわせて書き換えます。今回は文字列の`$title`が渡されますので、こんな風にします。

```php:app/Http/Controllers/PageController.php
public function show(string $title)
{
    //
}
```

でロジックを書き、

```php:app/Http/Controllers/PageController.php
public function show(string $title)
{
    // タイトルにマッチするページを取得する
    $page = Page::whereTitle($title)->first();

    return view('pages.show')->with([
        'page' => $page,
    ]);
}
```

ページ表示のためのテンプレートをつくる

```php:resources/views/pages/show.blade.php
@extends('app')

@section('content')
    <h1>{{ $page->title }}</h1>
    <div>
        {{ $page->body }}
    </div>
@endsection
```

そしてブラウザで`http://wiki.dev/pages/blanditiis`を確認すると・・・

<img width="749" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/f3bdc618-cd2f-0ac6-5d0f-4ea91706c8b2.png">

(´;ω;`)できたﾌﾞﾜｯ

## Implicit Binding

突然の横文字。

さて、こんな感じで一覧ページとパーマリンクの表示が一応できるようになりました。
先ほど`show()`メソッドの引数`$id`を`string $title`に変え且つページの取得ロジックを記述しましたが、これと同じ処理は他のメソッドでも必要となる予定です。

- `edit($id)`
- `update(Request $request, $id)`
- `destroy($id)`

これらについても先ほどと同様に変更すれば問題なく動作するのですが、ここではLaravelの機能をつかって、よりスマートな感じで解決してみることにします。

Laravelでは、ルーティングの情報から直接モデルを取得できる`Implicit Binding`という機能があります。今回の場合、ルーティング時に`$title`という文字列が取得できるので、それをキーとしてページモデルを解決できます。

`Page`モデルに以下のメソッドを追加することで`title`をキーとして指定します。（指定しない場合は`id`がキーとなります）

```php:app/Page.php
public function getRouteKeyName()
{
   return 'title';
}
```

アクションの引数で該当のモデルを指定することでこの機能が使われます。アクションを以下のように書き換えます。

```php:app/Http/Controllers/PageController.php
public function show(Page $page) ← ここ
{
   return view('pages.show')->with([
       'page' => $page,
    ]);
}
```

暗黙的にモデルを解決（束縛）するので`Implicit Binding`ということですね。
アクション実行時には既に該当するページの取得が完了しているので、だいぶスッキリと処理を書けるようになったと思います。Laravelすごい。

こういった`依存性注入`はLaravelの得意とするところで、うまく活用することできれいなコードを書くことができます。

他のメソッドの引数も同じように書き換えておきましょう。

- `edit(Page $page)`
- `update(Request $request, Page $page)`
- `destroy(Page $page)`

## ページを作成

ページを書く機能がまだありませんので、次はここに着手してみます。
ページの作成では、次のルーティングを使います。

| Verb | URI | Action | Route Name |
|:--|:--|:--|:--|
| GET | /pages/create | create | pages.create |
| POST | /pages | store | pages.store |

#### フォームを表示

まずは`GET /pages/create`を実装します。これはページの作成フォーム画面になります。
ブラウザで`/pages/create`にアクセスすると例によって白紙になるので、以下のような処理を記述します。

```php:app/Http/Controllers/PageController.php
public function create()
{
    return view('pages.form', [
       'page' => new Page(),
    ]);
}
```

テンプレートについて、今回はすこしトリッキーですが、新規作成と編集で同じものをつかうことにします。
`@if ($page->id)`の分岐で作成と編集を切り替えていて、 新規の場合は`POST`、編集の場合は`PUT`メソッドが用いられるようにしています。

```php:resources/views/pages/form.blade.php
@extends('app')

@section('content')
    <h1>Form</h1>

    @if (count($errors) > 0)
        <div class="alert alert-danger">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    @if ($page->id)
        <form action="/pages/{{ $page->title }}" method="POST">
            {{ method_field('PUT') }}
    @else
        <form action="/pages" method="POST">
    @endif
        {{ csrf_field() }}
        <div class="form-group @if($errors->has('title')) has-error @endif">
            <label class="control-label">Title</label>
            <input name="title" type="text" class="form-control" value="{{ old('title', $page->title) }}">
        </div>
        <div class="form-group @if($errors->has('body')) has-error @endif">
            <label class="control-label">Body</label>
            <textarea name="body" class="form-control" name="" id="" cols="30" rows="10">{{ old('body', $page->body) }}</textarea>
        </div>
        <input type="submit" class="form-control btn-primary">
    </form>

@endsection
```

ブラウザで`http://wiki.dev/pages/create`を確認すると無事フォームが表示されました。

<img width="749" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/50db4c54-05e0-a230-5210-42ee09210a65.png">

#### ページの作成

つづいて`POST /pages`の処理をコントローラに記述します。
フォームからのリクエストをバリデーションして、ページを作成して、できたページのURLにリダイレクトします。

```php:app/Http/Controllers/PageController.php
public function store(Request $request)
{
    // バリデーション
    $this->validate($request, [
        'title' => 'required|unique:pages,title',
        'body' => 'required',
    ]);

    // ページを作成
    $post = Page::create($request->all());

    // つくったページのURLにリダイレクト
    return redirect($post->url);
}
```

だいぶシンプルですね。ちなみにバリデーションに関しては、Laravelでは上記の汎用的な書き方以外にも、[Form Request Validation](https://laravel.com/docs/master/validation#form-request-validation)というフォームのバリデーション時に便利な機能も用意されていますので、ぜひチェックしてみてください。

#### 確認しよう

ブラウザで確認してみましょう。

<img width="745" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/7761a733-0c8f-2a76-08dd-4133896d79fd.png">

でポストしてみます。

<img width="750" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/4961422d-81cf-3cc3-5917-7c137fae7756.png">

(´;ω;`)できたﾌﾞﾜｯ

内容を空にしたり、既にあるものと同じタイトルで投稿しようとするとバリデーションエラーが発生し、正常にチェックが行われていることがわかります。

<img width="711" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/7d73f7fc-e130-96de-d430-d842671fecc4.png">

## ページを編集

つづいて編集機能をつくりましょう。作成のときと同じ感じでいけます。
利用するルーティングは以下のとおりです。

| Verb | URI | Action | Route Name |
|:--|:--|:--|:--|
| GET | /pages/{page}/edit | edit | pages.edit |
| PUT/PATCH | /pages/{page} | update | pages.update |

#### フォームを表示

`GET /pages/{page}/edit`を実装します。これはページ編集フォーム画面になります。

```php:app/Http/Controllers/PageController.php
public function edit(Page $page)
{
    return view('pages.form', [
       'page' => $page,
    ]);
}
```

フォームは先ほどつくったのでそのまま利用します。

#### ページの編集

アクションの実装についても、作成のときとほぼ同じ流れになります。
違いとしては、メソッドの引数として該当のページのインスタンスが渡されるので（前述した`Implicit Binding`で）、それを利用してバリデート(タイトルのuniqueチェック)とアップデートを行うところになります。
（uniqueチェックにIdを渡すことで自分自身を例外にしています）

```php:app/Http/Controllers/PageController.php
public function update(Request $request, Page $page)
{
    $this->validate($request, [
        'title' => 'required|unique:pages,title,'.$page->id,
        'body' => 'required',
    ]);

    $page->update($request->all());

    return redirect($page->url);
}
```

ブラウザで確認してみましょう。

スクショは割愛しますが (´;ω;`)できたﾌﾞﾜｯ ということでお願いします

## markdownに対応する

さて、いよいよお楽しみのmarkdownに対応するときがきました。
本来はここまでで前編くらいの予定でしたが、なんでしょう後編も既に長文感がでてきました。

それでは気分とページを書き換えて、試しにmarkdownぽいものを書いてみましょう。
さっきつくった「Hello World!」のページを編集してみます。

※編集のためのリンクは用意してないので、ページのパーマリンクでURLの末尾に`/edit`をつけてください

<img width="747" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/fffe4d3d-f6ba-536c-71c4-fbb0750c1447.png">

そしてポスト

<img width="748" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/da6f1726-6624-9d1c-70a0-e8ea150a9b80.png">

はい。

#### markdownライブラリを導入

PHPにもいろいろなmarkdownのライブラリがありますが、今回は「[cebe/markdown](https://github.com/cebe/markdown)」というものを使ってみます。

composerでインストールします。

```
$ composer require cebe/markdown
```

#### パースしてみる

`cebe/markdown`の使いかたは[ドキュメント](https://github.com/cebe/markdown)をみるとこんな感じのようです。

```php:github.com/cebe/markdownより
$parser = new \cebe\markdown\Markdown();
echo $parser->parse($markdown);
```

さっそくこれをつかってmarkdownのパースを実装してみましょう。`Post`モデルに自身の`body`をパースするための`parse()`メソッドを追加します。

`use`でインポートして

```php:app/Page.php
use cebe\markdown\Markdown as Markdown; ← 追加

class Page extends Model
{
```

`parse()`メソッドを追加

```php:app/Page.php
// bodyのmarkdownをパースする
public function parse()
{
    $parser = new Markdown();

    return $parser->parse($this->body);
}
```

さらにテンプレートからモデルの属性値として取得できるようにしておきます（URLのときにやったのと同じです）

```php:app/Page.php
public function getMarkdownBodyAttribute()
{
    return $this->parse();
}
```

そしてテンプレートの該当箇所を書き換える

```php:resources/views/pages/show.blade.php
@extends('app')

@section('content')
    <h1>{{ $page->title }}</h1>
    <div>
        {!! $page->markdown_body !!} ← ここ
    </div>
@endsection
```

※HTMLをそのまま表示するため、エスケープをしないリテラルに変えています

ブラウザで確認してみましょう

<img width="744" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/bb6a37d8-fa29-851c-b107-f0f76ffa1370.png">

(´;ω;`)できたﾌﾞﾜｯ

わりと簡単にみんな大好きなmarkdownに対応することができました！
ちなみに僕はmarkdownは難しいので苦手です。

## :star: 別ページへのリンクをはる(1)

さて次に、別ページへのリンクに対応してみましょう。「Laravelでwiki的なものをつくってみる」というこの記事において、**もっともwikiらしい**といえる機能がここであります。ハイライト感を出すためにタイトルにも星をつけてみました。

wikiではよく`[ページタイトル]`的な機能で別ページへのリンクを設けたりしますので、それになぞらえた感じの仕組みをつくってみましょう。

markdownで同じ書式にすると都合が悪そうな予感がするので、今回は次のような仕様にしてみます。

- `[[ページタイトル]]`という書式で別ページへのリンクをはることができる
- ページがまだ存在しなければ新規作成ページを表示 ← NEW

ちなみにNEWというのは僕の中で今さら気づいた新しいタスクという意味になります。

#### 独自タグへの対応

`[[ページタイトル]]`という独自タグを実装してみます。

`cebe/markdown`のドキュメントによると、このような場合は拡張クラスをつくるとのことなのでつくってみましょう。traitで機能を切り分けるとキレイそうですが、ひとまず今回はシンプルにこんな感じにしました。

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

namespace App;

use cebe\markdown\GithubMarkdown;

class WikiMarkdown extends GithubMarkdown
{
    public function __construct()
    {
        $this->enableNewlines = true;
        $this->html5 = true;
    }

    /**
     * @marker [[
     * @marker ]]
     */
    protected function parseBracketTag($markdown)
    {
        if (preg_match('/^\[\[(.+?)\]\]/', $markdown, $matches)) {
            return [
                ['bracket', $this->parseInline($matches[1])],
                strlen($matches[0]),
            ];
        }

        return [['text', substr($markdown, 0, 2)], 2];
    }

    protected function renderBracket($element)
    {
        $title = $this->renderAbsy($element[1]);
        $url = route('pages.show', $title);

        return sprintf('<a href="%s" class="router-link">%s</a>', $url, $title);
    }
}
```

`parseBracketTag()`と（`'bracket'`という名前を通してそこから呼び出される）`renderBracket()`が今回の`[[ページタイトル]]`に対応するためのコードになります。
当該の記法をページへのリンクに書き換えるという処理を行っています。

ちなみにPHPDoc的なコメントにある`@marker`はアノテーションとして機能するので、消すと動かなくなります。`@marker`で指定した文字列がみつかるとこのメソッドがよばれるようです。不気味ですね。

ついでに、改行を`<br>`として扱いたいので、`Markdown`クラスのかわりに`GithubMarkdown`クラスを継承し、コンストラクタでオプションの設定をしています。

これで独自タグをパースできるクラスができましたので、モデルのパース処理でこのクラスを利用するようにします。

```php:app/Page.php
// use cebe\markdown\GithubMarkdown as Markdown; ← これの代わりに
use App\WikiMarkdown as Markdown; ← これをインポートする
```

インポート時のクラス名は変えていないので、クラスを利用する箇所のコードはそのままでOKです。

それではブラウザで確認してみましょう。ドキドキ。
存在するページのタイトルをつかって独自タグを記述します。

<img width="747" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/429ed1dd-0fc9-43cf-ed92-1bffb91c5ebe.png">

こんな感じの内容でポストしてみましょう。

<img width="750" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/52cfcd58-5b77-8932-0da6-c1d4b9e4a565.png">

ちゃんとリンクになっていますね。クリックすると・・・

<img width="749" alt="スクリーンショット 2016-12-21 12.26.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/16630/5bbcd1dd-4923-1495-c91a-4bb9bd4b1c39.png">

(´;ω;`)できたﾌﾞﾜｯ

## :star2: 別ページへのリンクをはる(2)

うまく独自タグが機能するようになりました。
が、まだ無いページへのリンクがはられた場合のことをさっきまで考えていなかったので、その対応を行ってみましょう。

そもそもまだ無いページにアクセスするとどうなるのかを見てみることにします。
まずポストを書き換えて、存在しないページへのリンクをはります。

<img width="751" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/81250382-5d79-c1ab-dce0-c8756308e71d.png">

そしてリンクからアクセス

<img width="562" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/60b690b2-4990-3536-56e5-5d58b90e9584.png">

例外！ というわけで、モデルが見つからないというエラーになりました。この場合、例の`Implicit Binding`経由での処理になっているので、ルーティング時に取得した`$title`にマッチするページが見つからないということを言っています。

対応方法としてはいくつかありそうな気がするのですが、今回は素直にメソッドの処理を書き換えてみたいと思います。
該当の箇所は`PageController`の`show()`なので、ここを変えていきます。

まず現状はこの通りです。

```php:app/Http/Controllers/PageController.php
public function show(Page $page)
{
    return view('pages.show')->with([
        'page' => $page,
    ]);
}
```

ここで`Page`モデルを解決しようとしているのが原因なので、余計なことをせずに`$title`文字列を受け取るようにしてみます。

```php:app/Http/Controllers/PageController.php
public function show(string $title) ← ここ
{
    ...
```

ただ引数を書き換えただけ的な。
これでタイトルを受け取れるようになりました。モデルの解決が行われなくなったので、先ほどの例外はもう発生しないはずです。
かわりにモデルの解決を自分で行う必要ができましたので、その処理を書きます。

```php:app/Page.php
public function show(string $title)
{
    $page = Page::whereTitle($title)->first();

    return view('pages.show')->with([
        'page' => $page,
    ]);
}
```

このかたちはまさに本ページの一番上でやったパーマリンク機能の最初に書いた処理と同じですね。運命の再開。

さて、ページが見つからない場合どうするか？について考えます。いくつか選択肢はあって、いきなり新規作成ページに飛ばしてもいいし、ページが存在しないのでつくりますか？などと表示してもいいと思います。今回はよりwikiっぽい感じのする後者的アプローチをとることにします。

```php:app/Page.php
public function show(string $title)
{
    $page = Page::whereTitle($title)->first();

    if (!$page) {
        $page = new Page();
        $page->title = $title;
    }

    return view('pages.show')->with([
        'page' => $page,
    ]);
}
```

ページが見つからないときには新しいインスタンスを作成し、リクエストから渡される`$title`をタイトルに代入しています。
あとはページが存在するときと同様の処理になり、ビューにページのデータが渡されます。

それではブラウザで`http://wiki.dev/pages/ないページ`を確認してみましょう。

<img width="754" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/22521e32-ed57-de5b-b78d-e95cdba041fc.png">

(´;ω;`)できたﾌﾞﾜｯ

あとはテンプレートを少しいじって、それっぽいメッセージとリンクを追加してみましょう。

```php:resources/views/pages/show.blade.php
@extends('app')

@section('content')
    <h1>{{ $page->title }}</h1>
    @if ($page->id)
        <div>
            {!! $page->markdown_body !!}
        </div>
    @else
        <div class="well">
            この名前のページはまだ作成されていません
        </div>
        <div>
            <a href="{{ route('pages.create', ['title' => $page->title]) }}">ページを作成</a>
        </div>
    @endif
@endsection
```

そしてリロード

<img width="752" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/6d88c534-8df9-1420-76e0-8b6a4bb808ef.png">

(´;ω;`)できたﾌﾞﾜｯ

最後に、上のページの作成リンクには、`title`パラメータを仕込んでいるので、受け取り側もそれに対応しておきます。
フォームのタイトルの項目に、`title`で渡された文字列があらかじめ入力されるようにします。

```php:app/Http/Controllers/PageController.php
public function create(Request $request)
{
    // ここでインスタンスをつくって
    $page = new Page();
    // titleパラメータを受け取るようにする
    $page->title = $request->title;

    return view('pages.form', [
        'page' => $page,
    ]);
}
```

そしてブラウザで`http://wiki.dev/pages/create?title=ないページ`にアクセス

<img width="747" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/f1ab2ecd-6848-0068-f9f2-64b3faec7383.png">

やったーできたよ
(´;ω;`)ﾌﾞﾜｯ そして (´;ω;`)ﾌﾞﾜｯ

## 検索について あるいはBYEFORNOW

Laravelの[Scout](https://laravel.com/docs/master/scout)を使った検索を試したいと思っていましたが、今回はちょっと時間切れのためまたの機会に書きたいと思います。お楽しみに！

(*ﾉω・*)ﾃﾍ

ごめんなさい・・・＆ハッピーメリークリスマス！！ :christmas_tree: 

## まとめ

というわけでwiki的なものをつくってみました。
ちょっと駆け足気味だったり雑だったりするところもありましたが、Laravelを使ったアプリ作成の流れや便利さや面白さみたいなものが少しでも伝わったら幸いです。
この記事を読みながら実際につくってみると実はなんだこれ説明通りやっても動かねーよくそなどあると思いますので、ぜひつくってみていただけると楽しいのではないかと思います。

以上となります。TL;DR

## ソースコード

githubにうｐしてありますのでご参考まで :octocat: 

- https://github.com/ohida/laravel-wiki (master)
- https://github.com/ohida/laravel-wiki/tree/20161221 (今日時点のタグ)

## 耳寄り情報

#### 今日！SchooでLaravelの授業があるよ

[PHPフレームワーク（Laravel）を使った効率的なWebアプリケーション開発](https://schoo.jp/class/3651)

#### Schooではエンジニアのお友だちを募集しているよ
[来年のSchooアドベントカレンダーを一緒に書かないか](https://www.wantedly.com/companies/schooWEBcampus/projects?occupation_type=engineer)

(*ﾉω・*)ﾃﾍ