Schooアドベントカレンダーの21日目の記事です。
前編のまとめ
- 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に対応するメソッドの雛形をつくってくれていますが、当然ですがまだからっぽです。
public function show($id)
{
//
}
ここで指定されたページを表示するためのロジックを書きたいところですが、ちょっとまってください、$id
というのが渡ってきていますね。これはルーティング時にデフォルトではIDが渡されることを想定しているということで、実態にあわせて書き換えます。今回は文字列の$title
が渡されますので、こんな風にします。
public function show(string $title)
{
//
}
でロジックを書き、
public function show(string $title)
{
// タイトルにマッチするページを取得する
$page = Page::whereTitle($title)->first();
return view('pages.show')->with([
'page' => $page,
]);
}
ページ表示のためのテンプレートをつくる
@extends('app')
@section('content')
<h1>{{ $page->title }}</h1>
<div>
{{ $page->body }}
</div>
@endsection
そしてブラウザでhttp://wiki.dev/pages/blanditiis
を確認すると・・・
(´;ω;`)できたブワッ
Implicit Binding
突然の横文字。
さて、こんな感じで一覧ページとパーマリンクの表示が一応できるようになりました。
先ほどshow()
メソッドの引数$id
をstring $title
に変え且つページの取得ロジックを記述しましたが、これと同じ処理は他のメソッドでも必要となる予定です。
edit($id)
update(Request $request, $id)
destroy($id)
これらについても先ほどと同様に変更すれば問題なく動作するのですが、ここではLaravelの機能をつかって、よりスマートな感じで解決してみることにします。
Laravelでは、ルーティングの情報から直接モデルを取得できるImplicit Binding
という機能があります。今回の場合、ルーティング時に$title
という文字列が取得できるので、それをキーとしてページモデルを解決できます。
Page
モデルに以下のメソッドを追加することでtitle
をキーとして指定します。(指定しない場合はid
がキーとなります)
public function getRouteKeyName()
{
return 'title';
}
アクションの引数で該当のモデルを指定することでこの機能が使われます。アクションを以下のように書き換えます。
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
にアクセスすると例によって白紙になるので、以下のような処理を記述します。
public function create()
{
return view('pages.form', [
'page' => new Page(),
]);
}
テンプレートについて、今回はすこしトリッキーですが、新規作成と編集で同じものをつかうことにします。
@if ($page->id)
の分岐で作成と編集を切り替えていて、 新規の場合はPOST
、編集の場合はPUT
メソッドが用いられるようにしています。
@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
を確認すると無事フォームが表示されました。
ページの作成
つづいてPOST /pages
の処理をコントローラに記述します。
フォームからのリクエストをバリデーションして、ページを作成して、できたページのURLにリダイレクトします。
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というフォームのバリデーション時に便利な機能も用意されていますので、ぜひチェックしてみてください。
確認しよう
ブラウザで確認してみましょう。
でポストしてみます。
(´;ω;`)できたブワッ
内容を空にしたり、既にあるものと同じタイトルで投稿しようとするとバリデーションエラーが発生し、正常にチェックが行われていることがわかります。
ページを編集
つづいて編集機能をつくりましょう。作成のときと同じ感じでいけます。
利用するルーティングは以下のとおりです。
Verb | URI | Action | Route Name |
---|---|---|---|
GET | /pages/{page}/edit | edit | pages.edit |
PUT/PATCH | /pages/{page} | update | pages.update |
フォームを表示
GET /pages/{page}/edit
を実装します。これはページ編集フォーム画面になります。
public function edit(Page $page)
{
return view('pages.form', [
'page' => $page,
]);
}
フォームは先ほどつくったのでそのまま利用します。
ページの編集
アクションの実装についても、作成のときとほぼ同じ流れになります。
違いとしては、メソッドの引数として該当のページのインスタンスが渡されるので(前述したImplicit Binding
で)、それを利用してバリデート(タイトルのuniqueチェック)とアップデートを行うところになります。
(uniqueチェックにIdを渡すことで自分自身を例外にしています)
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
をつけてください
そしてポスト
はい。
markdownライブラリを導入
PHPにもいろいろなmarkdownのライブラリがありますが、今回は「cebe/markdown」というものを使ってみます。
composerでインストールします。
$ composer require cebe/markdown
パースしてみる
cebe/markdown
の使いかたはドキュメントをみるとこんな感じのようです。
$parser = new \cebe\markdown\Markdown();
echo $parser->parse($markdown);
さっそくこれをつかってmarkdownのパースを実装してみましょう。Post
モデルに自身のbody
をパースするためのparse()
メソッドを追加します。
use
でインポートして
use cebe\markdown\Markdown as Markdown; ← 追加
class Page extends Model
{
parse()
メソッドを追加
// bodyのmarkdownをパースする
public function parse()
{
$parser = new Markdown();
return $parser->parse($this->body);
}
さらにテンプレートからモデルの属性値として取得できるようにしておきます(URLのときにやったのと同じです)
public function getMarkdownBodyAttribute()
{
return $this->parse();
}
そしてテンプレートの該当箇所を書き換える
@extends('app')
@section('content')
<h1>{{ $page->title }}</h1>
<div>
{!! $page->markdown_body !!} ← ここ
</div>
@endsection
※HTMLをそのまま表示するため、エスケープをしないリテラルに変えています
ブラウザで確認してみましょう
(´;ω;`)できたブワッ
わりと簡単にみんな大好きなmarkdownに対応することができました!
ちなみに僕はmarkdownは難しいので苦手です。
別ページへのリンクをはる(1)
さて次に、別ページへのリンクに対応してみましょう。「Laravelでwiki的なものをつくってみる」というこの記事において、もっともwikiらしいといえる機能がここであります。ハイライト感を出すためにタイトルにも星をつけてみました。
wikiではよく[ページタイトル]
的な機能で別ページへのリンクを設けたりしますので、それになぞらえた感じの仕組みをつくってみましょう。
markdownで同じ書式にすると都合が悪そうな予感がするので、今回は次のような仕様にしてみます。
-
[[ページタイトル]]
という書式で別ページへのリンクをはることができる - ページがまだ存在しなければ新規作成ページを表示 ← NEW
ちなみにNEWというのは僕の中で今さら気づいた新しいタスクという意味になります。
独自タグへの対応
[[ページタイトル]]
という独自タグを実装してみます。
cebe/markdown
のドキュメントによると、このような場合は拡張クラスをつくるとのことなのでつくってみましょう。traitで機能を切り分けるとキレイそうですが、ひとまず今回はシンプルにこんな感じにしました。
<?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
クラスを継承し、コンストラクタでオプションの設定をしています。
これで独自タグをパースできるクラスができましたので、モデルのパース処理でこのクラスを利用するようにします。
// use cebe\markdown\GithubMarkdown as Markdown; ← これの代わりに
use App\WikiMarkdown as Markdown; ← これをインポートする
インポート時のクラス名は変えていないので、クラスを利用する箇所のコードはそのままでOKです。
それではブラウザで確認してみましょう。ドキドキ。
存在するページのタイトルをつかって独自タグを記述します。
こんな感じの内容でポストしてみましょう。
ちゃんとリンクになっていますね。クリックすると・・・
(´;ω;`)できたブワッ
別ページへのリンクをはる(2)
うまく独自タグが機能するようになりました。
が、まだ無いページへのリンクがはられた場合のことをさっきまで考えていなかったので、その対応を行ってみましょう。
そもそもまだ無いページにアクセスするとどうなるのかを見てみることにします。
まずポストを書き換えて、存在しないページへのリンクをはります。
そしてリンクからアクセス
例外! というわけで、モデルが見つからないというエラーになりました。この場合、例のImplicit Binding
経由での処理になっているので、ルーティング時に取得した$title
にマッチするページが見つからないということを言っています。
対応方法としてはいくつかありそうな気がするのですが、今回は素直にメソッドの処理を書き換えてみたいと思います。
該当の箇所はPageController
のshow()
なので、ここを変えていきます。
まず現状はこの通りです。
public function show(Page $page)
{
return view('pages.show')->with([
'page' => $page,
]);
}
ここでPage
モデルを解決しようとしているのが原因なので、余計なことをせずに$title
文字列を受け取るようにしてみます。
public function show(string $title) ← ここ
{
...
ただ引数を書き換えただけ的な。
これでタイトルを受け取れるようになりました。モデルの解決が行われなくなったので、先ほどの例外はもう発生しないはずです。
かわりにモデルの解決を自分で行う必要ができましたので、その処理を書きます。
public function show(string $title)
{
$page = Page::whereTitle($title)->first();
return view('pages.show')->with([
'page' => $page,
]);
}
このかたちはまさに本ページの一番上でやったパーマリンク機能の最初に書いた処理と同じですね。運命の再開。
さて、ページが見つからない場合どうするか?について考えます。いくつか選択肢はあって、いきなり新規作成ページに飛ばしてもいいし、ページが存在しないのでつくりますか?などと表示してもいいと思います。今回はよりwikiっぽい感じのする後者的アプローチをとることにします。
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/ないページ
を確認してみましょう。
(´;ω;`)できたブワッ
あとはテンプレートを少しいじって、それっぽいメッセージとリンクを追加してみましょう。
@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
そしてリロード
(´;ω;`)できたブワッ
最後に、上のページの作成リンクには、title
パラメータを仕込んでいるので、受け取り側もそれに対応しておきます。
フォームのタイトルの項目に、title
で渡された文字列があらかじめ入力されるようにします。
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=ないページ
にアクセス
やったーできたよ
(´;ω;)ブワッ そして (´;ω;
)ブワッ
検索について あるいはBYEFORNOW
LaravelのScoutを使った検索を試したいと思っていましたが、今回はちょっと時間切れのためまたの機会に書きたいと思います。お楽しみに!
(ノω・)テヘ
ごめんなさい・・・&ハッピーメリークリスマス!!
まとめ
というわけでwiki的なものをつくってみました。
ちょっと駆け足気味だったり雑だったりするところもありましたが、Laravelを使ったアプリ作成の流れや便利さや面白さみたいなものが少しでも伝わったら幸いです。
この記事を読みながら実際につくってみると実はなんだこれ説明通りやっても動かねーよくそなどあると思いますので、ぜひつくってみていただけると楽しいのではないかと思います。
以上となります。TL;DR
ソースコード
githubにうpしてありますのでご参考まで
- https://github.com/ohida/laravel-wiki (master)
- https://github.com/ohida/laravel-wiki/tree/20161221 (今日時点のタグ)
耳寄り情報
今日!SchooでLaravelの授業があるよ
PHPフレームワーク(Laravel)を使った効率的なWebアプリケーション開発
Schooではエンジニアのお友だちを募集しているよ
(ノω・)テヘ