Laravel5でシンプルなCRUDアプリを開発する

こんにちは〜たきゃはしです〜最近もホントにとにかくビール最高!って感じです!
今回はついにリリースされたLaravel5(以降はL5と略記する)を早速使ってみました!

◯ この記事の概要

L5の基本機能を扱いつつシンプルなブログアプリを作ってみようと思います。

こんな感じになります

この記事の目的はブログアプリの開発を通してL5でCRUDを作れるようになることです。構成をなるべくシンプルにしたかったこともあり沢山の機能は取り扱ったわけではありませんが「PHPやMySQLは分かるけどFWはよくわからないな〜」とか「Laravel5 気になってるんだよな〜」という人には特におすすめですよ!

さっそくインストールからはじめたいと思います。

◯ インストール

 


$ composer create-project laravel/laravel l5blog --prefer-dist

l5blog というディレクトリを作成しインストールします
以降に登場するファイルパスはこの l5blog を基準とします。

◯ 初期設定

 

・ storageのパーミッションの変更

 


$ chmod -R 777 storage

 

・ データベースの設定

・ .env


DB_HOST=localhost
DB_DATABASE=l5blog
DB_USERNAME=root
DB_PASSWORD=secret

データベースのドライバーはデフォルトで mysql です。
他のドライバーを使用したい場合は config/database.php で設定してください。

◯ データベースの作成

 


mysql> CREATE DATABASE `l5blog`;

 

◯ マイグレーション

ここでいうマイグレーションとは、データベースのテーブルやカラムの追加や変更の定義を管理することを指します。

・ マイグレーションファイルの生成

 


$ php artisan make:migration create_articles_table
Created Migration: 2015_02_17_114821_create_articles_table

 

・ テーブル定義の作成

・ database/migrations/2015_02_17_114821_create_articles_table.php


<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('articles');
    }
}

 

・ マイグレーションの実行

 


$ php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2015_02_17_114821_create_articles_table

これでデータベースに articles というテーブルが追加されました。
同時にいくつか追加されたテーブルがありますが今回使用しないため気にする必要はありません。

◯ シーダー(初期データ)

シーダーを生成するコマンドは標準でないので通常通りファイルを作成します。

・ 記事シーダーの作成

・ database/seeds/ArticlesTableSeeder.php


<?php
use Illuminate\Database\Seeder;
class ArticlesTableSeeder extends Seeder
{
    public function run()
    {
        DB::table('articles')->truncate();
        DB::table('articles')->insert([
            [
                'title'      => 'Laozi',
                'body'       => 'When there is no desire, all things are at peace.',
                'created_at' => '2015-01-31 23:59:59',
                'updated_at' => '2015-01-31 23:59:59',
            ],
            [
                'title'      => 'Leonardo da Vinci',
                'body'       => 'Simplicity is the ultimate sophistication.',
                'created_at' => '2015-02-01 00:00:00',
                'updated_at' => '2015-02-01 00:00:00',
            ],
            [
                'title'      => 'Cedric Bledsoe',
                'body'       => 'Simplicity is the essence of happiness.',
                'created_at' => '2015-02-01 00:00:01',
                'updated_at' => '2015-02-01 00:00:01',
            ],
        ]);
    }
}

記事シーダーを作成したら下記のコマンドを実行してください。


$ composer dump-autoload
//もしくは
$ php artisan optimize

※ 8egsさん、ご報告ありがとうございます!

・ database/seeds/DatabaseSeeder.php


<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();
        $this->call('ArticlesTableSeeder');
    }
}

 

・ シーダーの実行

 


$ php artisan db:seed
Seeded: ArticlesTableSeeder

デフォルトでDatabaseSeederが実行されるので記事データがインサートされます。
2回目以降であれば下記コマンドが便利です。


$ php artisan migrate:refresh --seed
Rolled back: 2015_02_17_114821_create_articles_table
Rolled back: 2014_10_12_100000_create_password_resets_table
Rolled back: 2014_10_12_000000_create_users_table
Nothing to rollback.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2015_02_17_114821_create_articles_table
Seeded: ArticlesTableSeeder

いよいよブログアプリの作成に進みます。

◯ 記事モデルの作成

はじめはモデルを作りましょう。


$ php artisan make:model Article
Model created successfully.

さっそく生成されたArticleモデルを確認してみます。

・ app/Article.php


<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
    //
}

モデルはappディレクトリ直下に生成されます。モデルがアプリの中枢を担うことになりますので妥当かもしれませんね。もちろん新たにModelディレクトリを作成してそこに配置してもLaravelでは何も問題ありません。

ひな形へ肉付けしていきます。

・ app/Article.php


<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'articles';
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['title', 'body'];
}

対応するテーブル名や保存が可能なフィールドを定義しました。(テーブル名はDRYに従っていれば実質不要なのですが明示的にするため記載しました。)
もし時間があるならばAricleモデルが継承している Illuminate\Database\Eloquent\Model を確認してみてください。他にどのようなプロパティが設定できるのか知っておくことは大切です。例えば今回定義しなかった「$perPage」はページネーションで1ページに表示する件数を指定することができます。

◯ コントローラー

コントローラーのひな形を生成します。


$ php artisan make:controller ArticlesController
Controller created successfully.

生成されたファイルはリソースコントローラーを前提とするアクションがずらっと定義されています。ですが今回は暗黙的コントローラーとして実装していきたいので一旦すべてのメソッドを削除してください。(暗黙的コントローラーについては後ほど簡単に説明します)

・ app/Http/Controllers/ArticlesController.php


<?php namespace App\Http\Controllers;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ArticlesController extends Controller
{
}

すっきりしたところで 一覧、詳細、作成、編集、削除 のアクションをそれぞれ定義していきます。


<?php namespace App\Http\Controllers;
use App\Article;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ArticlesController extends Controller
{
   /**
    * @var Article
    */
   protected $article;
   /**
    * @param Article $article
    */
   public function __construct(Article $article)
   {
       $this->article = $article;
   }
   public function getIndex()
   {
   }
   public function getShow()
   {
   }
   public function getCreate()
   {
   }
   public function postCreate()
   {
   }
   public function getEdit()
   {
   }
   public function postEdit()
   {
   }
   public function postDelete()
   {
   }
}

暗黙的コントローラーでは、URIとHTTPメソッドで対応するアクションが決定します。上記ではgetCreate()とpostCreate()では対応するURIは同じになりますが対応するHTTPメソッドが異なります。またPATCHやDELETEといった他のHTTPメソッドにも対応可能ですが今回は割愛します。

◯ ルーティング

作成したコントローラーをルーティングへ追加します。
'/'へのアクセスは記事一覧、'/articles'は暗黙的コントローラーとして定義します。

・ app/Http/routes.php


<?php
Route::get('/', 'ArticlesController@getIndex');
Route::controller('articles', 'ArticlesController');

ここでコントローラーのネームスペースが指定する必要がないのは、 routes.php が App\Providers\RouteServiceProvider で読み込まれているためです。App\Providers\RouteServiceProviderで 'App\Http\Controllers' をネームスペースとするグループのルーティングだと定義されているおかげです。

◯ 記事の一覧

次に一覧の作成に取り掛かります。インサートした記事をすべて表示させましょう。
すべて記事を取得してビューへ渡すアクションを作成します。

・ 記事一覧アクション

 


<?php ...
    /**
     * 記事の一覧
     *
     * @return \Illuminate\View\View
     */
    public function getIndex()
    {
        $articles = $this->article->all();
        return view('articles.index')->with(compact('articles'));
    }

view()の中身はドット繋ぎでディレクトリ構造を表現できます。with()にはビューで使いたいデータを配列で渡してあげればOKです。

次にビューを作成します。テンプレートエンジンはLaravelデファクトのBladeを使用します。
アプリらしくViewExtends、共通のビューと個別のビューを別々に作成します。

・ 共通のビュー

・ resources/views/app.blade.php


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>L5Blog</title>
    <link href="/css/app.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="row">
            <h1>L5Blog</h1>
            <div class="col-md-12">
                @yield('content')
            </div>
        </div>
    </div>
</body>
</html>

 

・ 記事一覧のビュー(個別のビュー)

・ resources/views/articles/index.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事一覧</h2>
    <table class="table table-striped table-hover">
        <thead>
        <tr>
            <th>タイトル</th>
            <th>本文</th>
            <th>作成日時</th>
            <th>更新日時</th>
        </tr>
        </thead>
        <tbody>
        @foreach($articles as $article)
            <tr>
                   <td>{{{ $article->title }}}</td>
                   <td>{{{ $article->body }}}</td>
                   <td>{{{ $article->created_at }}}</td>
                   <td>{{{ $article->updated_at }}}</td>
            </tr>
        @endforeach
        </tbody>
    </table>
@endsection

ホストのルート'/'へアクセスしてみてください!一覧が表示されるはずです!

◯ 記事の詳細

次は記事の詳細を表示するページを作成していきます。

・ 記事詳細アクション

 


<?php ...
    /**
     * 記事の詳細
     *
     * @param $id
     * @return \Illuminate\View\View
     */
    public function getShow($id)
    {
        $article = $this->article->find($id);
        return view('articles.show', compact('article'));
    }

データはview()の第二引数から渡す方法もあります。

・ 記事詳細のビュー

・ resources/views/articles/show.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事詳細</h2>
    <table class="table table-striped">
        <tbody>
        <tr>
            <th>タイトル</th>
            <td>{{{ $article->title }}}</td>
        </tr>
        <tr>
            <th>本文</th>
            <td>{{{ $article->body }}}</td>
        </tr>
        <tr>
            <th>作成日時</th>
            <td>{{{ $article->created_at }}}</td>
        </tr>
        <tr>
            <th>更新日時</th>
            <td>{{{ $article->updated_at }}}</td>
        </tr>
        </tbody>
    </table>
@endsection

これで 'articles/show/2' へアクセスしてみてください!暗黙的コントローラーにより id:2 の記事の詳細が表示されます!

記事の一覧に詳細ページへのリンクを追加してみましょう。

・ resources/views/articles/index.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事一覧</h2>
    <table class="table table-striped table-hover">
        <thead>
        <tr>
            <th>タイトル</th>
            <th>本文</th>
            <th>作成日時</th>
            <th>更新日時</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        @foreach($articles as $article)
            <tr>
                   <td>{{{ $article->title }}}</td>
                   <td>{{{ $article->body }}}</td>
                   <td>{{{ $article->created_at }}}</td>
                   <td>{{{ $article->updated_at }}}</td>
                   <td><a href="/articles/show/{{{ $article->id }}}" class="btn btn-default btn-xs">詳細</a></td>
            </tr>
        @endforeach
        </tbody>
    </table>
@endsection

一覧から詳細リンクをクリックすると詳細ページに遷移されるようになりました。
Laravelが初めての方でもなんとなくイメージは掴めてきたでしょうか?

次は投稿処理です。

◯ 記事の投稿

投稿は一覧や詳細と違いPOSTを受け付けます。

- GET /articles/create の場合は getCreate()
- POST /articles/create の場合は postCreate()

上記を理解していればアクションとフォームで何をしなくてはいけないかハッキリしますね。

・ 記事投稿のアクション

 


<?php ...
    /**
     * 記事の投稿
     *
     * @return \Illuminate\View\View
     */
    public function getCreate()
    {
        return view('articles.create');
    }
    /**
     * 記事の投稿
     *
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function postCreate(Request $request)
    {
        $data = $request->all();
        $this->article->fill($data);
        $this->article->save();
        return redirect()->to('articles/index');
    }

L5ではアクションなどでメソッドインジェクションによる依存性の注入が可能です。

・ 記事投稿のビュー

と、ここで問題発生です。

記事投稿用のフォームの作成をしていきますが、ワタクシここでハマりました。。。
L4ではFormクラスとHTMLクラスといういわゆるヘルパークラスがありそれを使おうとしてもなぜか使えずに悩んでいました。IDEでファサードが補完されない時点で「オヤ?」と思いサービスプロバイダーを確認したところ見当たらずロードされていないことに気が付きました。どうやらL5からFormクラスとHTMLクラスは統合されなくなったようで別途Composerによるインストールが必要なことが分かりました。

という訳で、あった方が便利なので準備します。

インストール

 


$ composer require "laravelcollective/html:5.0.*"

 

サービスプロバイダーとファサードの追加

・ config/app.php


<?php ...
    'Illuminate\Hashing\HashServiceProvider',
    Collective\Html\HtmlServiceProvider::class,
    'Illuminate\Mail\MailServiceProvider',

ファサードも変更します。

・ config/app.php


<?php ...
    'File'      => 'Illuminate\Support\Facades\File',
    'Form'      => Collective\Html\FormFacade::class,
    'Hash'      => 'Illuminate\Support\Facades\Hash',
    'HTML'      => Collective\Html\HtmlFacade::class,
    'Input'     => 'Illuminate\Support\Facades\Input',

これでHTMLクラスとFormクラスの準備は完了です。
L4からやってる人はハマるだろうなぁと思いました。笑

※ L5.1 リリース頃にまたリポジトリが変わってしまったことの指摘があり修正しました。トイキン君さんありがとうございます。

気を取り直して、記事投稿ビューの作成です。

・ resources/views/articles/create.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事投稿</h2>
    {!! Form::open() !!}
        <div class="form-group">
            <label>タイトル</label>
            {!! Form::input('text', 'title', null, ['required', 'class' => 'form-control']) !!}
        </div>
        <div class="form-group">
            <label>本文</label>
            {!! Form::textarea('body', null, ['required', 'class' => 'form-control']) !!}
        </div>
        <button type="submit" class="btn btn-default">投稿</button>
    {!! Form::close() !!}
@endsection

'/articles/create'へアクセスしてください!フォームが表示され投稿が可能になったはずです。

◯ 記事の編集

編集は既に投稿を作っているため特筆することがありませんが、せっかくなのでソースを見ないで自分で作ってみるのも理解への近道かもしれませんね。

・ 記事編集のアクション

 


<?php ...
    /**
     * 記事の編集
     *
     * @param $id
     * @return \Illuminate\View\View
     */
    public function getEdit($id)
    {
        $article = $this->article->find($id);
        return view('articles.edit')->withArticle($article);
    }
    /**
     * 記事の編集
     *
     * @param Request $request
     * @param         $id
     * @return \Illuminate\Http\RedirectResponse
     */
    public function postEdit(Request $request, $id)
    {
        $article = $this->article->find($id);
        $data = $request->all();
        $article->fill($data);
        $article->save();
        return redirect()->to('articles/index');
    }

with()はマジックメソッドとしても利用可能です。
上記の withArticle($article) は with('article' => $article) と同じです。

・ 記事編集のビュー

・ resources/views/articles/edit.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事編集</h2>
    {!! Form::open(['action' => ['ArticlesController@postEdit', $article->id]]) !!}
        <div class="form-group">
            <label>タイトル</label>
            {!! Form::input('text', 'title', $article->title, ['required', 'class' => 'form-control']) !!}
        </div>
        <div class="form-group">
            <label>本文</label>
            {!! Form::textarea('body', $article->body, ['required', 'class' => 'form-control']) !!}
        </div>
        <button type="submit" class="btn btn-default">編集</button>
    {!! Form::close() !!}
@endsection

編集ページヘの導線を作成しましょう。(先程の投稿もついでに)

・ resources/views/articles/index.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事一覧</h2>
    <div>
        <a href="/articles/create" class="btn btn-primary">投稿</a>
    </div>
    <table class="table table-striped table-hover">
        <thead>
        <tr>
            <th>タイトル</th>
            <th>本文</th>
            <th>作成日時</th>
            <th>更新日時</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        @foreach($articles as $article)
            <tr>
                <td>{{{ $article->title }}}</td>
                <td>{{{ $article->body }}}</td>
                <td>{{{ $article->created_at }}}</td>
                <td>{{{ $article->updated_at }}}</td>
                <td>
                    <a href="/articles/show/{{{ $article->id }}}" class="btn btn-default btn-xs">詳細</a>
                    <a href="/articles/edit/{{{ $article->id }}}" class="btn btn-success btn-xs">編集</a>
                </td>
            </tr>
        @endforeach
        </tbody>
    </table>
@endsection

これで編集も出来ましたね!最後は削除です。

◯ 記事の削除

もしGETで受け付けた場合は'/articles/delete/1'にアクセスした時点で削除されます。それは厳しいと思いますのでPOSTでのみ受け付けることにします。

・ 記事削除のアクション

 


<?php ...
    /**
     * 記事の削除
     *
     * @param $id
     * @return \Illuminate\Http\RedirectResponse
     */
    public function postDelete($id)
    {
        $article = $this->article->find($id);
        $article->delete();
        return redirect()->to('articles/index');
    }

 

・ 記事削除のビュー

・ resources/views/articles/index.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事一覧</h2>
    <div>
        <a href="/articles/create" class="btn btn-primary">投稿</a>
    </div>
    <table class="table table-striped table-hover">
        <thead>
        <tr>
            <th>タイトル</th>
            <th>本文</th>
            <th>作成日時</th>
            <th>更新日時</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        @foreach($articles as $article)
            <tr>
                <td>{{{ $article->title }}}</td>
                <td>{{{ $article->body }}}</td>
                <td>{{{ $article->created_at }}}</td>
                <td>{{{ $article->updated_at }}}</td>
                <td>
                    <a href="/articles/show/{{{ $article->id }}}" class="btn btn-default btn-xs">詳細</a>
                    <a href="/articles/edit/{{{ $article->id }}}" class="btn btn-success btn-xs">編集</a>
                    {!! Form::open(['action' => ['ArticlesController@postDelete', $article->id]]) !!}
                    <button type="submit" class="btn btn-danger btn-xs">削除</button>
                    {!! Form::close() !!}
                </td>
            </tr>
        @endforeach
        </tbody>
    </table>
@endsection

・ resources/views/articles/show.blade.php


@extends('app')
@section('content')
    <h2 class="page-header">記事詳細</h2>
    <ul class="list-inline">
        <li>
            <a href="/articles/edit/{{{ $article->id }}}" class="btn btn-primary pull-left">編集</a>
        </li>
        <li>
            {!! Form::open(['action' => ['ArticlesController@postDelete', $article->id]]) !!}
            <button type="submit" class="btn btn-danger pull-left">削除</button>
            {!! Form::close() !!}
        </li>
    </ul>
    <table class="table table-striped">
        <tbody>
        <tr>
            <th>タイトル</th>
            <td>{{{ $article->title }}}</td>
        </tr>
        <tr>
            <th>本文</th>
            <td>{{{ $article->body }}}</td>
        </tr>
        <tr>
            <th>作成日時</th>
            <td>{{{ $article->created_at }}}</td>
        </tr>
        <tr>
            <th>更新日時</th>
            <td>{{{ $article->updated_at }}}</td>
        </tr>
        </tbody>
    </table>
@endsection

削除ボタンをクリックすればデータが削除されるようになりましたでしょうか?
確認ボックスもなしにイキナリ削除なんて驚いたと思いますが、これにてブログアプリは完成です!お疲れさまでした〜!

◯ まとめ

L5でブログアプリを開発してみましたがどうでしょうか?正直シンプルすぎる構成ゆえL5の魅力をお伝え出来たとは思っていません(自分も全然満足していません)。個人的に注目しているのは新しくなったバリデーション、扱いやすくなったサービスプロバイダー、モデルとルーティングのバインド、メソッドインジェクションによる依存性の注入、他にも様々な機能が盛りだくさんで正直一記事で説明するのは無理があると思いました。なので、それらも今後発信していけたらと思います。

記事のソースでエラーが出たとかあればコメントください!それでは〜!(ソースは欲しい方いれば公開します。)