Laravelで使えるスキャフォールドl5scaffoldのインストールと使い方

Laravel

スキャフォールドとは

スキャフォールドとは、元々英語で足場という意味があります。

Webアプリケーション開発に関しては、MVCフレームワークで開発する際に必要なモデルやコントローラー、ビューをコマンドなどで自動生成してくれる機能のことです。

スキャフォールドを使用すると、簡単にアプリケーションの足場(雛形)を作ることができるので開発の工数を抑えたりすることができるのです。

l5scaffoldのインストール

公式サイト

こちらがl5scaffoldの公式サイト(Githubレポジトリ)になります。

基本はこの公式サイトのREADMEがわかりやすいため、公式サイトの手順をベースに説明していきます。

ただl5scaffoldの開発やバグフィックスが終了しているようで、公式サイトの手順だけだとエラーが発生してスキャフォールドを使用することができません。

そのため発生したエラーの解消方法もわかりやすくまとめているので是非参考にしてください!

Composerでl5scaffoldをインストール

l5scaffoldのインストールは、Composerを使用すれば簡単です。

Laravelプロジェクト直下のcomposer.jsoncomposer.lockファイルがあるディクレトリへ移動してこちらのコマンドを打てばインストール完了です。

composer require laralib/l5scaffold --dev

その際は、必ず–devオプションをつけて開発環境用にインストールしましょう!

スキャフォールドは、開発時のみ使用して本番環境では使用しないためです。

Composerで開発環境と本番環境を分ける方法がわからない方は、こちらに詳しく解説しているので参照してください!

l5scaffoldをサービスプロバイダーへ登録する

次はインストールしたl5scaffoldをLaravelで使えるようにするために、サービスプロバイダーへ登録していきます

このようにconfig/app.phpdev-providersのセクションにl5scaffoldを追加してください!

config/app.php

    // 開発環境でのみ必要なプロバイダー
    'dev-providers' => [
        Laralib\L5scaffold\GeneratorsServiceProvider::class,
    ],

config/app.phpdev-providersのセクションがないという方は、こちの記事で紹介している方法で作成しましょう!

そのままデフォルトで存在するprovidersのセクションへ追加してしまうと、composerの–no-devオプションで開発環境専用にインストールしたl5scaffoldは本番環境へは導入されていないため、本番環境でL5scaffoldが見つからずエラーになってしまうので注意して下さい。

上記の記事で、providerやaliasの環境分けを実施すればこのような問題も発生しなくなります。

Artisanを実行してスキャフォールディングをしてみよう!

ここまでの手順でL5scaffoldのインストールが完了しています。

試しに以下のコマンドで、スキャフォールドを実行してみましょう!

php artisan make:scaffold Tweet --schema="title:string:default('Tweet #1'), body:text"

するとエラーがでると思います。
※本記事の掲載: 2019/05/22

これらのエラーは既にGithubのIssuesにバグとして上がっており、修正されるはずなのですがどうやら開発やバグフィックスが止まってしまっていて、修正が取り込まれていないようです。

安心してください!

エラーの解決方法については、次の項にて詳しく説明しています。

エラーを解決し、スキャフォールドでCRUD画面をどんどん自動生成しちゃってください!

エラーを解消する

本項では、L5scaffoldをインストール後に発生するエラーの解決方法を見ていきます。

エラー1: ScaffoldMakeCommand.php:21

エラー内容

PHP Fatal error:  Trait 'Illuminate\Console\AppNamespaceDetectorTrait' not found in /home/ubuntu/ダウンロード/test/stress_check/vendor/laralib/l5scaffold/src/Commands/ScaffoldMakeCommand.php on line 21

In ScaffoldMakeCommand.php line 21:
                                                                  
  Trait 'Illuminate\Console\AppNamespaceDetectorTrait' not found  

l5scaffoldのインストールで解説しているインストール完了後のスキャフォールディングの実行コマンドを実行する、または、既存のレポジトリからgit cloneしてきてcomposer installした時に発生するのが恐らくこのエラーだと思います。

対応方法

エラー内容は、ScaffoldMakeCommand.phpの21行目にあるトレイトAppNamespaceDetectorTraitが見つからないためエラーが発生しています。

以下のサンプルコードにあるように、エラーが発生している箇所をコメントアウトし、DetectsApplicationNamespaceを使用するように処理を追加しましょう!

vendor/laralib/l5scaffold/src/Commands/ScaffoldMakeCommand.php

// use Illuminate\Console\AppNamespaceDetectorTrait;
use Illuminate\Console\DetectsApplicationNamespace;


class ScaffoldMakeCommand extends Command
{
    // use AppNamespaceDetectorTrait, MakerTrait;
    use DetectsApplicationNamespace;

エラー2: ScaffoldMakeCommand::handle()

エラー内容

In BoundMethod.php line 135:
                                                                               
  Method Laralib\L5scaffold\Commands\ScaffoldMakeCommand::handle() does not e  
  xist

この時点で再度l5scaffoldのインストールに記載のスキャフォールディング実行コマンドを実行するとこちらのエラーが発生します。

対応方法

エラー内容は、ScaffoldMakeCommand.phphandle()メソッドが存在しないために発生しています。

以下のサンプルのように、ScaffoldMakeCommandクラス内のコンストラクタの下あたりに、handleメソッドを追加すればOKです。

vendor/laralib/l5scaffold/src/Commands/ScaffoldMakeCommand.php

class ScaffoldMakeCommand extends Command
{
    .
    .
    public function __construct(Filesystem $files, Composer $composer)
    {
    }

    /**
     * Handle the console command.
     * To support Laravel >= 5.5
     *
     * @return mixed
     */
    public function handle()
    {
        $this->fire();
    }

エラー3: MakeController.php:13

エラー内容

こちらも再度スキャフォールディング実行コマンドを実行するとこちらのエラーが発生します。

In MakeController.php line 13:
                                                                  
  Trait 'Illuminate\Console\AppNamespaceDetectorTrait' not found  

対応方法

エラー内容は、1回目に発生したエラーと同様にAppNamespaceDetectorTraitが見つからないためエラーが発生しています。

MakeController.phpの13行目にあるAppNamespaceDetectorTraitのトレイトをuseしている箇所をコメントアウトし、先程と同様にDetectsApplicationNamespaceを使用するように処理を追加しましょう!

vendor/laralib/l5scaffold/src/Makes/MakeController.php

// use Illuminate\Console\AppNamespaceDetectorTrait;
use Illuminate\Console\DetectsApplicationNamespace;


class MakeController
{
    // use AppNamespaceDetectorTrait, MakerTrait;
    use DetectsApplicationNamespace, MakerTrait;

これでエラーの対応が完了して、エラーが解消されているはずです!

事項で再度スキャフォールディングを実行して試してみましょう!

再度スキャフォールディングをしてみてエラーが解消したか確認しよう!

以下のコマンドを再度実行して、エラーが発生しなければ見事スキャフォールドが使えるようになりました!

php artisan make:scaffold Tweet --schema="title:string:default('Tweet #1'), body:text"

l5scaffoldの使い方

スキャフォールドのエラーが解消したところで、次は実際の使い方を確認していきましょう!

スキャフォールドのコマンドを実行する

スキャフォールドの実行はこのように、make:scaffoldで実行することができます。

php artisan make:scaffold Tweet --schema="title:string:default('Tweet #1'), body:text"

生成されるファイル

ここでは、上記のマイグレーションコマンドでスキャフォールドにより自動生成されるファイル群を記載しておきました。

これだけのプログラムファイルがコマンド一発で自動生成されるなんて、かなり便利ですね..!

マイグレーションファイル

database/migrations/2019_05_22_015424_create_tweets_table.php

<?php

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

class CreateTweetsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('tweets', function(Blueprint $table) {
            $table->increments('id');
            $table->string('title')->default('Tweet #1');
            $table->text('body');
            $table->timestamps();
        });
	}

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

}

ビュー

resources/views/layout.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">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="favicon.ico">

    <title>Starter Template</title>

    <!-- Bootstrap core CSS -->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">

    <!-- Custom styles for this template -->
    <!-- <link href="starter-template.css" rel="stylesheet"> -->


    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
    @yield('css')
</head>

<body>

    <nav class="navbar navbar-default">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">Project name</a>
            </div>
            <div id="navbar" class="collapse navbar-collapse">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="#">Home</a></li>
                    <li><a href="#about">About</a></li>
                    <li><a href="#contact">Contact</a></li>
                </ul>
            </div><!--/.nav-collapse -->
        </div>
    </nav>

    <div class="container">
        @yield('header')
        @yield('content')
    </div><!-- /.container -->

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
    <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
    <!-- <script src="../../assets/js/ie10-viewport-bug-workaround.js"></script> -->
    @yield('scripts')
</body>
</html>

resources/views/tweets/create.blade.php

@extends('layout')
@section('css')
  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.0/css/bootstrap-datepicker.css" rel="stylesheet">
@endsection
@section('header')
    <div class="page-header">
        <h1><i class="glyphicon glyphicon-plus"></i> Tweets / Create </h1>
    </div>
@endsection

@section('content')
    @include('error')

    <div class="row">
        <div class="col-md-12">

            <form action="{{ route('tweets.store') }}" method="POST">
                <input type="hidden" name="_token" value="{{ csrf_token() }}">

                <div class="form-group @if($errors->has('title')) has-error @endif">
                       <label for="title-field">Title</label>
                    <input type="text" id="title-field" name="title" class="form-control" value="{{ old("title") }}"/>
                       @if($errors->has("title"))
                        <span class="help-block">{{ $errors->first("title") }}</span>
                       @endif
                    </div>
                    <div class="form-group @if($errors->has('body')) has-error @endif">
                       <label for="body-field">Body</label>
                    <textarea class="form-control" id="body-field" rows="3" name="body">{{ old("body") }}</textarea>
                       @if($errors->has("body"))
                        <span class="help-block">{{ $errors->first("body") }}</span>
                       @endif
                    </div>
                <div class="well well-sm">
                    <button type="submit" class="btn btn-primary">Create</button>
                    <a class="btn btn-link pull-right" href="{{ route('tweets.index') }}"><i class="glyphicon glyphicon-backward"></i> Back</a>
                </div>
            </form>

        </div>
    </div>
@endsection
@section('scripts')
  <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.0/js/bootstrap-datepicker.min.js"></script>
  <script>
    $('.date-picker').datepicker({
    });
  </script>
@endsection

resources/views/tweets/edit.blade.php

@extends('layout')
@section('css')
  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.0/css/bootstrap-datepicker.css" rel="stylesheet">
@endsection
@section('header')
    <div class="page-header">
        <h1><i class="glyphicon glyphicon-edit"></i> Tweets / Edit #{{$tweet->id}}</h1>
    </div>
@endsection

@section('content')
    @include('error')

    <div class="row">
        <div class="col-md-12">

            <form action="{{ route('tweets.update', $tweet->id) }}" method="POST">
                <input type="hidden" name="_method" value="PUT">
                <input type="hidden" name="_token" value="{{ csrf_token() }}">

                <div class="form-group @if($errors->has('title')) has-error @endif">
                       <label for="title-field">Title</label>
                    <input type="text" id="title-field" name="title" class="form-control" value="{{ is_null(old("title")) ? $tweet->title : old("title") }}"/>
                       @if($errors->has("title"))
                        <span class="help-block">{{ $errors->first("title") }}</span>
                       @endif
                    </div>
                    <div class="form-group @if($errors->has('body')) has-error @endif">
                       <label for="body-field">Body</label>
                    <textarea class="form-control" id="body-field" rows="3" name="body">{{ is_null(old("body")) ? $tweet->body : old("body") }}</textarea>
                       @if($errors->has("body"))
                        <span class="help-block">{{ $errors->first("body") }}</span>
                       @endif
                    </div>
                <div class="well well-sm">
                    <button type="submit" class="btn btn-primary">Save</button>
                    <a class="btn btn-link pull-right" href="{{ route('tweets.index') }}"><i class="glyphicon glyphicon-backward"></i>  Back</a>
                </div>
            </form>

        </div>
    </div>
@endsection
@section('scripts')
  <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.0/js/bootstrap-datepicker.min.js"></script>
  <script>
    $('.date-picker').datepicker({
    });
  </script>
@endsection

resources/views/tweets/index.blade.php

@extends('layout')

@section('header')
    <div class="page-header clearfix">
        <h1>
            <i class="glyphicon glyphicon-align-justify"></i> Tweets
            <a class="btn btn-success pull-right" href="{{ route('tweets.create') }}"><i class="glyphicon glyphicon-plus"></i> Create</a>
        </h1>

    </div>
@endsection

@section('content')
    <div class="row">
        <div class="col-md-12">
            @if($tweets->count())
                <table class="table table-condensed table-striped">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>TITLE</th>
                        <th>BODY</th>
                            <th class="text-right">OPTIONS</th>
                        </tr>
                    </thead>

                    <tbody>
                        @foreach($tweets as $tweet)
                            <tr>
                                <td>{{$tweet->id}}</td>
                                <td>{{$tweet->title}}</td>
                    <td>{{$tweet->body}}</td>
                                <td class="text-right">
                                    <a class="btn btn-xs btn-primary" href="{{ route('tweets.show', $tweet->id) }}"><i class="glyphicon glyphicon-eye-open"></i> View</a>
                                    <a class="btn btn-xs btn-warning" href="{{ route('tweets.edit', $tweet->id) }}"><i class="glyphicon glyphicon-edit"></i> Edit</a>
                                    <form action="{{ route('tweets.destroy', $tweet->id) }}" method="POST" style="display: inline;" onsubmit="if(confirm('Delete? Are you sure?')) { return true } else {return false };">
                                        <input type="hidden" name="_method" value="DELETE">
                                        <input type="hidden" name="_token" value="{{ csrf_token() }}">
                                        <button type="submit" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash"></i> Delete</button>
                                    </form>
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
                {!! $tweets->render() !!}
            @else
                <h3 class="text-center alert alert-info">Empty!</h3>
            @endif

        </div>
    </div>

@endsection

resources/views/tweets/show.blade.php

@extends('layout')
@section('header')
<div class="page-header">
        <h1>Tweets / Show #{{$tweet->id}}</h1>
        <form action="{{ route('tweets.destroy', $tweet->id) }}" method="POST" style="display: inline;" onsubmit="if(confirm('Delete? Are you sure?')) { return true } else {return false };">
            <input type="hidden" name="_method" value="DELETE">
            <input type="hidden" name="_token" value="{{ csrf_token() }}">
            <div class="btn-group pull-right" role="group" aria-label="...">
                <a class="btn btn-warning btn-group" role="group" href="{{ route('tweets.edit', $tweet->id) }}"><i class="glyphicon glyphicon-edit"></i> Edit</a>
                <button type="submit" class="btn btn-danger">Delete <i class="glyphicon glyphicon-trash"></i></button>
            </div>
        </form>
    </div>
@endsection

@section('content')
    <div class="row">
        <div class="col-md-12">

            <form action="#">
                <div class="form-group">
                    <label for="nome">ID</label>
                    <p class="form-control-static"></p>
                </div>
                <div class="form-group">
                     <label for="title">TITLE</label>
                     <p class="form-control-static">{{$tweet->title}}</p>
                </div>
                    <div class="form-group">
                     <label for="body">BODY</label>
                     <p class="form-control-static">{{$tweet->body}}</p>
                </div>
            </form>

            <a class="btn btn-link" href="{{ route('tweets.index') }}"><i class="glyphicon glyphicon-backward"></i>  Back</a>

        </div>
    </div>

@endsection

コントローラー

app/Http/Controllers/TweetController.php

<?php namespace App\Http\Controllers;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use App\Tweet;
use Illuminate\Http\Request;

class TweetController extends Controller {

	/**
	 * Display a listing of the resource.
	 *
	 * @return Response
	 */
	public function index()
	{
		$tweets = Tweet::orderBy('id', 'desc')->paginate(10);

		return view('tweets.index', compact('tweets'));
	}

	/**
	 * Show the form for creating a new resource.
	 *
	 * @return Response
	 */
	public function create()
	{
		return view('tweets.create');
	}

	/**
	 * Store a newly created resource in storage.
	 *
	 * @param Request $request
	 * @return Response
	 */
	public function store(Request $request)
	{
		$tweet = new Tweet();

		$tweet->title = $request->input("title");
        $tweet->body = $request->input("body");

		$tweet->save();

		return redirect()->route('tweets.index')->with('message', 'Item created successfully.');
	}

	/**
	 * Display the specified resource.
	 *
	 * @param  int  $id
	 * @return Response
	 */
	public function show($id)
	{
		$tweet = Tweet::findOrFail($id);

		return view('tweets.show', compact('tweet'));
	}

	/**
	 * Show the form for editing the specified resource.
	 *
	 * @param  int  $id
	 * @return Response
	 */
	public function edit($id)
	{
		$tweet = Tweet::findOrFail($id);

		return view('tweets.edit', compact('tweet'));
	}

	/**
	 * Update the specified resource in storage.
	 *
	 * @param  int  $id
	 * @param Request $request
	 * @return Response
	 */
	public function update(Request $request, $id)
	{
		$tweet = Tweet::findOrFail($id);

		$tweet->title = $request->input("title");
        $tweet->body = $request->input("body");

		$tweet->save();

		return redirect()->route('tweets.index')->with('message', 'Item updated successfully.');
	}

	/**
	 * Remove the specified resource from storage.
	 *
	 * @param  int  $id
	 * @return Response
	 */
	public function destroy($id)
	{
		$tweet = Tweet::findOrFail($id);
		$tweet->delete();

		return redirect()->route('tweets.index')->with('message', 'Item deleted successfully.');
	}

}

モデル

app/Tweet.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tweet extends Model
{
    //
}

シーダー

database/seeds/TweetTableSeeder.php

<?php

use Illuminate\Database\Seeder;

// composer require laracasts/testdummy
use Laracasts\TestDummy\Factory as TestDummy;

class TweetTableSeeder extends Seeder {

    public function run()
    {
        // TestDummy::times(20)->create('App\Post');
    }

}

マイグレーションの実行

自動生成されたファイルたちを実際に動作させるには、まず手動でマイグレーションを実行をする必要があります。

php artisan migrate

ルーティングの追加

続いてルーティングをする必要があります。

ここでは、http://127.0.0.1:8000/tweetsにアクセスした時にスキャフォールドで作成した画面へいけるようにルーティングをしてみます。

routes/web.php

Route::resource("tweets","TweetController");

作成したページへアクセスしてみよう

http://127.0.0.1:8000/tweetsにアクセスすると、ツイートをイメージしたサンプルが自動生成されている。

実際の画面へアクセスするとこんな感じです。

index

create

show

destroy

まとめ

いかがでしたでしょうか。

今回は、プログラムや画面を自動生成する技術であるスキャフォールドについて解説しました。

スキャフォールドは業務でも開発工数を抑えるため、マスタデータを投入する際の画面などを自動生成したりしますし、個人でも簡単な処理を爆速で作成できるため使いこなすと便利です。

自動生成した処理や画面では物足りないということが多々あるかと思いますが、自動生成したものをベースに処理も画面もカスタマイズしていけば、かなり便利です!

Laravelでスキャフォールドも使いこなして、さらにLaravelのレベルを上げていきましょう!

Laravel始めて間もない方は、Laravelの上達方法についても詳しく記事をまとめていますので見てみてくださいね!

Laravelの学習をこれから始めたい!という方は、Laravelの入門記事も書いていますので是非見て下さい!

Laravelでオススメな本を探している!という方は、こちらで僕がLaravelの書籍をまとめて感想を書いていますので購入の際の参考にしてください!

コメント