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

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

## はじめに

#### わたしについて
こんにちは、Schooの@ohidaです。2016年9月にSchooにジョインし、イノベーション推進室及びプロダクト戦略室という部署で、新規事業を推進したりSchooをよりイケてるサービスにするお仕事をしています。会社ではてへぺろちゃんと呼ばれています。

私は開発チーム所属のバリバリのエンジニア**ではない**のでSchooのエンジニアリングの凄みについてはあまり語れません。ので、今回はこれから社内での導入を企んでいるお気に入りフレームワークのLaravelのことについて書きたいと思います。

#### なんの話をするか

[Laravel](https://laravel.com/docs/5.3)というPHPのフレームワークを使って簡単なウェブアプリケーションをひとつつくってみようと思います。Laravelの基本的な機能の一部+αでつくる予定です。ウェブアプリをつくるのってこんな感じで楽しいですよというのをお伝えできればと。

- つくるもの = Laravelを使った簡単なウェブアプリ
- 対象読者 = Laravelをこれから始める人
- 言いたいこと = ウェブアプリつくるのたのしいお

ちなみにアドベントカレンダー2日分のノルマを満たすため、前編と後編を予定していますが、何を書くかよりも先に2日分書くということを決めたので、尺が余ったり足りなかったりするかもしれません(*ﾉω・*)ﾃﾍ

## つくるもの

今回は、wiki的な情報保存ツールをつくってみようと思います。
個人的に情報系ツールが好きなのと、シンプルながら一応動くはずなので、つくったあと実際に使ってみながら好きなように改造していく楽しみがあるかなーと思っています。

ひとまずこんな感じのものになる予定です。

#### こんな感じ
- キーワード毎にページをつくれる
- markdown記法に対応
- 別ページへのリンクがはれる
- ページの一覧ページがある
- 検索ができる

いかがでしょうか。
それっぽいものになりそうでドキドキしますね。

## 開発環境をつくる

さて、前置きはこのあたりにして、さっそく開発にはいっていきます。

#### Valetで環境構築

開発環境の構築はだいぶラクになってきましたが、仮想マシンを導入したりネットワークの設定を行うのはそれなりに手間がかかったりします。
ValetはLaravel向けに用意されている開発環境のひとつで、非常に簡単に環境を構築することができます。

https://laravel.com/docs/master/valet
こちらの公式ドキュメントを見ながら構築を行いましょう。
Homebrewを使ってインストールをしていきます。

```sh:homebrewでPHP7.1をインストール
$ brew install homebrew/php/php71
```

```sh:homebrewでValetをインストール
$ composer global require laravel/valet
(※ このあと ~/.composer/vendor/bin にパスを通しておきます)
```

```sh:Valetを動かす
$ valet install
```

ここでは`~/Code`フォルダをプロジェクト置き場に指定します。

```sh:~/Codeフォルダをプロジェクト置き場に指定
$ cd ~/Code
$ valet park
```

これだけで、`~/Code`以下に作成したLaravelのプロジェクトに`http://プロジェクト名.dev`というURLでアクセスできる準備ができました。hostsファイルとかいじらなくていいんです。ちなみにnginx x php-fpmという環境で動作します。

※ ValetはMacでのみ提供されています
※ ああ？ここまで読んだけどWindowsなんだけどｳｨﾝ！？という人は怒らず焦らず[Homestead](https://laravel.com/docs/master/homestead)という別の手段をご利用ください

#### データベース

データの保存先としてデータベースも使いますので、まだ何も入っていなければMariaDBをいれておいてください。

```sh:homebrewでMariaDBをインストール
$ brew install mariadb
$ brew services start mariadb
```

## プロジェクトをつくる

開発環境ができたら、次にLaravelの新規プロジェクトを作成しましょう。今回は"wiki"という名前にしてみます。

```sh:wikiという名前の新規プロジェクトを作成
$ cd ~/Code
$ laravel new wiki
```

プロジェクトの作成が完了したら、ブラウザで`http://wiki.dev/`にアクセスしてみます。
（Homesteadの場合は`http://wiki.app/`などに適宜読み替えてください）

<img width="665" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/40b94162-5dc7-0d9a-80ab-fc1a1bc01a9f.png">

できました！(*ﾉω・*)ﾃﾍ

これでLaravelのプロジェクトとして立派に動くようになりました。すごい。
続いて、今回の目的であるwiki的なものとしてのロジックをつくっていきます。

## ルーティングを設定する

まず[ルーティング](https://laravel.com/docs/master/routing)の設定を行うことにします。

今は`/`にアクセスするとデフォルトのウェルカムページが表示されます。この処理は、`routes/web.php`というファイルで指定されています。

```php:デフォルトのルーティング
Route::get('/', function () { // "/"にアクセスされたら
    return view('welcome');   // welcomeというビューを表示する
});
```

ルーティングを変えるには、このファイルを編集します。
Laravelには、よくあるCRUDに対応したルーティングを自動で行ってくれる機能があるので、今回はそれを利用してみましょう。

```php:routes.phpにこれを追加
Route::resource('pages', PageController::class);
```

これによって一気に以下のルートが利用できるようになります。

| Verb | URI | Action | Route Name |
|:--|:--|:--|:--|
| GET | /pages | index | pages.index |
| GET | /pages/create | create | pages.create |
| POST | /pages | store | pages.store |
| GET | /pages/{page} | show | pages.show |
| GET | /pages/{page}/edit | edit | pages.edit |
| PUT/PATCH | /pages/{page} | update | pages.update |
| DELETE | /pages/{page} | destroy | pages.destroy |

ためしにブラウザで`http://wiki.dev/pages`にアクセスしてみましょう。`GET /pages`なので`index`アクションがよばれるはずです。

```text:例外発生！
ReflectionException in Container.php line 749:
Class App\Http\Controllers\PageController does not exist
```

例外！(*ﾉω・*)ﾃﾍ
メッセージを見ると、さきほど指定した`PageController`が存在しないと言われています。

## コントローラをつくる

そんなわけで、さきほど設定したルーティングには、対応するコントローラが必要となります。
Laravelではコントローラを生成するコマンドが用意されています。CRUDに対応するメソッドをもったコントローラを作成するには、ターミナルから以下のコマンドを使います。

```sh:PageControllerを作成する
$ php artisan make:controller PageController --resource
```

以下のファイルが生成されます

- `app/Http/Controllers/PageController.php`

これで再度`http://wiki.dev/pages`にアクセスしてみましょう。すると、今度は白紙のページが表示されました。

実際にどのような処理が行われているのか、コントローラの内部を見てみます。前述のルーティングテーブルをみると`GET`で`pages`にアクセスすると`index`というアクションがよばれると書かれています。

| Verb | URI | Action | Route Name |
|:--|:--|:--|:--|
| GET | /pages | index | pages.index |

これは`PageController`の`index()`というメソッドが実行される、ということを意味していますので、該当のメソッドを確認してみます。

```php:app/Http/Controllers/PageController.php
public function index()
{
    //
}
```

何もしていない。
ためしに、以下の文を記述してみましょう。

```php:app/Http/Controllers/PageController.php
public function index()
{
    dd(__METHOD__); ←これを追加
}
```

ブラウザで確認してみるとメソッド名が表示され、ちゃんとこの処理が呼ばれているということがわかります。
（`dd()`はLaravelの[ヘルパー関数](https://laravel.com/docs/master/helpers)で変数のダンプに使われます）

このメソッドの役割は「ページの一覧を表示すること」なので、そうさせたいところですが、まだこのアプリはページという概念を持っていませんでした。(*ﾉω・*)ﾃﾍ

## Pageモデルをつくる

ページの概念を扱うため、`Page`モデルを作成しましょう。
Laravelでは以下のコマンドでモデルとマイグレーションを一緒に作成できます。

```sh
$ php artisan make:model Page --migration
```

以下のファイルが生成されます

- `app/Page.php`
- `database/migrations/2016_12_20_000000_create_pages_table.php`

#### マイグレーション

ページテーブルに「タイトル(title)」と「本文(body)」のカラムを持たせましょう。マイグレーションファイルに次の２行を追加します。

```php:database/migrations/2016_12_20_000000_create_pages_table.php
class CreatePagesTable extends Migration
{
    public function up()
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title')->unique(); ←追加
            $table->text('body'); ←追加
            $table->timestamps();
        });
    }
```

#### データベースの作成

まだデータベースをつくっていませんでした(*ﾉω・*)ﾃﾍ
ので、`wiki`というデータベースを作成します。

```sh:mysqlのデータベースをつくる
$ mysql
MariaDB > CREATE DATABASE wiki DEFAULT CHARACTER SET utf8;
```

#### .envの編集

Laravelからデータベースに接続するため`.env`ファイルを編集します

```
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=wiki
DB_USERNAME=root
DB_PASSWORD=
```

- 接続情報はご自身の環境に添った内容を記述してください
- 今回はMariaDB(mysql)を使っていますがSQLiteなどでももちろん大丈夫です

#### マイグレーション実行

これでデータベースが利用できるようになりましたのでマイグレーションを実行して、ページ用のテーブルを作成します。

```sh:レッツマイグレーション！
$ php artisan migrate
```

#### Pageモデルのfillable属性を設定

最後に`Page`モデルの`fillable`属性を設定します。
これによって、`title`と`body`についての一括代入([Mass Assignment](https://laravel.com/docs/master/eloquent#mass-assignment))が可能となります。

```php:app/Page.php
class Page extends Model
{
    protected $fillable = [
        'title', 'body',
    ];

    ...
```

#### Page用のモデルファクトリーをつくる

さて、これで`Page`モデルが使えるようになりました。
ついでなのでLaravelが持つ[モデルファクトリー](https://laravel.com/docs/master/database-testing#writing-factories)という便利機能を使ってテストデータを投入してみましょう。

モデルファクトリー（`factories/ModelFactory.php`）はプロジェクト作成時に用意されているので、以下を追加します。
これは、`App\Page`モデルのテストデータをつくるときの指定で、`title`と`body`に任意のダミーデータを代入しています。

```php:factories/ModelFactory.php
$factory->define(App\Page::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->word(),
        'body' => $faker->paragraphs(3, true),
    ];
});
```

※今回テストの話はしませんが、モデルファクトリーはテストの際に大活躍しますのでモデルとセットでつくっておくとよいかと思います。

### テストデータを投入

ではモデルファクトリーを使ってテストデータを投入してみましょう。

まずLaravelのREPLを起動します。

```sh
$ php artisan tinker
```

REPLの中でモデルファクトリーを実行します。
これは`App\Page`モデルをのデータを10個つくる、という意味になります。

```text:App\Pageを10個生成
>>> factory(App\Page::class, 10)->create();
```

```text:確認
>>> App\Page::count(); → 10
```

※ もしtitleのunique制約でエラーになる場合はもう一度実行してみてください・・・(*ﾉω・*)ﾃﾍ
※ データベースを初期状態に戻すには`php artisan migrate:refresh`などを利用できます

## ページ一覧を表示

#### アクションの実装

無事`Page`モデルとそのデータができましたので、コントローラの実装に戻りましょう。

`PageController`で`App\Page`モデルを使いやすくするため、`use`で名前空間をインポートします。

```php:app/Http/Controllers/PageController.php
<?php

namespace App\Http\Controllers;

use App\Page; ← 追加
use Illuminate\Http\Request;
```

次に`Page`モデルを使い、ページの一覧を取得します。`Page::all();`というメソッドで、すべてのページを取得することができます。
ここでも先程の`dd()`ヘルパー関数を使って中身を覗いてみましょう。

```php:app/Http/Controllers/PageController.php
public function index()
{
   $pages = Page::all();

   dd($pages);
}
```

ブラウザで確認すると以下のように10個のデータをもったCollectionオブジェクトであることが確認できると思います。

```
Collection {#184 ▼
  #items: array:10 [▶]
}
```

:bulb: `⌘`キーを押しながら`▶`をクリックするとネストの全レベルを展開できます。

[Collection](https://laravel.com/docs/master/eloquent-collections)はLaravelが用意している、データ配列を便利に扱うクラスで、この例のようにデータベースから一覧を取得した場合などに返されます。[`iterable`](https://wiki.php.net/rfc/iterable)なオブジェクトなので通常の配列と同じように扱うことができます。

#### ビューをつくる

さて、これでページの一覧データが取得できるようになったので、ブラウザに整形して表示してみたくなりますね。うずうず。

テンプレートをつくって表示してみましょう。
Laravelには[Blade](https://laravel.com/docs/master/blade)というシンプルながら強力なテンプレート機能がついているので、それを利用します。

一覧ページとパーマリンクという異なるページを用意する必要がありそうなので、ベースとなるテンプレートをつくっておいて、それをページ毎に継承するようにしてみます。

##### ベーステンプレート

アプリケーション共通でベースとなるテンプレートです。

```php:resources/views/app.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ config('app.name') }}<</title>
    <link href="/css/app.css" rel="stylesheet">
    <script>
        window.Laravel = <?php echo json_encode([
            'csrfToken' => csrf_token(),
        ]); ?>
    </script>
</head>
<body>
    @include('navbar')
    <div class="container">
        <div class="content">
            @yield('content')
        </div>
    </div>
    <script src="/js/app.js"></script>
</body>
</html>
```

##### ナビバー部分
navbarの部分も構造を明確にするために切り分けておきます。

```php:resources/views/navbar.blade.php
<nav class="navbar navbar-default">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="#">{{ config('app.name') }}<</a>
        </div>
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
            </ul>
        </div>
    </div>
</nav>
```

##### CSSもか

`resources/assets/sass/app.scss`の下の方に以下を追加して、`gulp`コマンドでCSSファイルをコンパイルします。

```sass:resources/assets/sass/app.scss
.content {
  background: #fff;
  padding: 15px;

  h1 {
    margin-bottom: 30px;
  }
}
```

※`gulp`がうごかない場合は

```sh
$ npm install --global gulp-cli
$ npm install
```
くわしくは[ドキュメント](https://laravel.com/docs/master/elixir)をご参照ください

##### ポスト一覧ページ

これが今回使うポスト一覧ページのテンプレートです。
ページの一覧データ`$pages`を`@foreach`でまわしてリストとして展開します。

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

@section('content')
    <h1>Home</h1>
    <ul>
        @foreach ($pages as $page)
            <li>
                {{ $page->title }}
            </li>
        @endforeach
    </ul>
@endsection
```

#### コントローラからビューを呼び出す

先ほど`dd()`でダンプしていたところを書き換えて、ビューを呼び出すように変更します。

```php:app/Http/Controllers/PageController.php
public function index()
{
    $pages = Page::all();

    // ↓これを追加
    return view('pages.index')->with([
        'pages' => $pages,
    ]);
}
```

ブラウザで確認してみると・・・

<img width="213" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/c14d602b-0371-6e24-4a92-19b0a534923c.png">

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

#### リンクをはる

ページ一覧の表示ができたので、次にページのタイトルにリンクを設定しましょう。パーマリンクのURLが必要となるので、`Page`モデルに自分のURLを返すメソッドを追加してみます。

ルーティングのテーブルを確認してみると、ページのパーマリンクは`pages.show`という名前がついていますので、この名前を使ってURLを生成します。

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

ルート名からURLをつくるには、`route()`というヘルパー関数を使います。URLを返すメソッドを追加しましょう。

```php:app/Page.php
public function url()
{
    return route('pages.show', $this->title);
}
```

さらに、Laravelでは属性のかたちで任意の値を取得できる機能がついていますので、それを使ってみます。
例えば`$page->url`という属性で先ほどの`url()`メソッドの結果を返すようにするにはこのように書きます。

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

それではこちらを使ってリンクをはってみましょう。ページ一覧のテンプレートのリストの各項目にアンカータグを設定します。

```php:resources/views/pages/index.blade.php
@foreach ($pages as $page)
    <li>
        <a href="{{ $page->url }}"> ←ここ
            {{ $page->title }}
        </a>
    </li>
@endforeach
```

<img width="214" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16630/5b432cf9-6b41-4c91-0e55-8455cfd28db7.png">

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

## TO BE CONTINUED

というところで、つかれたので続きは後編で書きたいと思います。ていうかなんだろう遠い。

#### 後編の予告
- パーマリンクを表示するよ
- ページを作成・編集するよ
- markdownに対応するよ
- markdownをカスタマイズするよ
- 検索するよ
- なんか終わらなそう

## 耳寄り情報

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

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

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

(*ﾉω・*)ﾃﾍ

[後編はこちら](http://qiita.com/ohida/items/f5280ccbb10f9b43f92c)
ソースコードもあるよ