laravel 開発日記 第11回 ~ カスタムコマンドの作成(スキャフォールド Scaffold)前編 ~

前回、シンプルなCRUDアプリを作りましたが、 なかなか、作業数が多く、骨が折れました。

ということで、今回は、コマンド1発でCRUDアプリを作る カスタムコマンドを作りたいと思います。

ちなみに、こういうのって開発の世界では、 スキャフォールド(Scaffold)っていうらしいですね。cakePHPとか、RubyOnRailsではよくつかわれている模様。 ちなみにlaravelにもすでにあったりします。

■PHPフレームワークLaravelやってみた
http://www.webopixel.net/php/824.html

今回は、お勉強もかねて自前で作ります。
なお、完成したファイル一式を以下にアップしてます。 参考まで。

■myapp
ダウンロード では、始めます。

まず、laravelのartisan開発について。

■Laravel 5.0.0 Artisan開発(公式)
http://readouble.com/laravel/5/0/0/ja/commands.html

■コマンドラインアプリケーション(ララ帳)
https://laravel10.wordpress.com/2015/05/13/%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/#more-1823

カスタムコマンド作成にあたり、以下2ファイルを修正します。

・/app/Console/Commands/MyAppCommand.php (新規)
・/app/Console/Kernel.php (修正)

まずは、簡単なKernel.phpから。

【作業ファイル:/app/Console/Kernel.php】

protected $commands = [
    'App\Console\Commands\Inspire',
    'App\Console\Commands\MyAppCommand',  //追加
];

カスタムコマンドとして呼び出すファイルを記述します。 で、コマンド本体のMyAppCommand.php。 長いので、順を追ってみていきます。

【作業ファイル:/app/Console/Commands/MyAppCommand.php】

<?php namespace App\Console\Commands;

use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;

use Illuminate\Support\Str;//(1)

(1)ですが、文字列操作用のAPIがあるので使いました。

■Str(laravel5 api)
http://laravel.com/api/5.0/Illuminate/Support/Str.html

protected $appName;    //snake_case

/*
Str::camel($this->appName);                                    //lowerCamel
Str::studly($this->appName);                                //UpperCamel

Str::plural($this->appName);                                //snake_cases
Str::camel(Str::plural($this->appName));        //lowerCamels
Str::studly(Str::plural($this->appName));        //UpperCamels
*/

$appNameにスネークケースをいれて、 各ローワーキャメル、アッパーキャメル、またその複数形も取得してます。 そのあと、public function fire()は、コマンド処理本体なので、ちょっとおいときます。

protected function getArguments()
{
    return [
        ['name', InputArgument::REQUIRED, 'entry app name with snake case'],
    ];
}

protected function getOptions()
{
    return [
        ['create', 'c', InputOption::VALUE_NONE, 'create my App.', null],
        ['delete', 'd', InputOption::VALUE_NONE, 'delete my App.', null],
    ];
}

アーギュメントとオプションの定義。

php artisan [カスタムコマンド名] [アーギュメント] [–オプション]
※ショートコードの時は[-オプション] で、呼び出します。

今回、アーギュメントにはアプリ名をスネークケースで、 また、オプションは、–create(-c)で作成、–delete(-d)で削除するように設定しました。

では、飛ばしていた処理本体に戻ります。

public function fire()
{

    //入力値の取得
    $this->appName = $this->argument('name');

    //appNameがスネークケースかどうか、チェックする
    preg_match('/[a-z0-9_]*/', $this->appName, $result);
    
    if( $this->appName !== $result[0] )    //小文字アルファベット、半角英数字、「_」以外の文字を含むとき
    {
        $this->error('スネークケースで入力してください。');
        exit();
    }

    if( !$this->option('create') && !$this->option('delete') )
    {
        $this->error('create または delete オプションをつけてください。');
        exit();
    }

最初に入力値の取得とチェックを行ってます。
・appNameがスネークケースか?
・-c または、-d オプションのどちらかがついているか?

//クリエイトモード、デリートモードの切り替え
if($this->option('create')) {    //クリエートモード

    /* ----------------------------------------
    (1)modelの作成(migrationも自動作成)
    -------------------------------------------*/

    $model_path = './app/'. Str::studly($this->appName). '.php';

    if( !file_exists($model_path) )    //モデルが無いとき
    {
        $this->call('make:model', ['name' => Str::studly($this->appName)]);

        //modelにマスアサインメント追加
        $model_text = file_get_contents( $model_path );
        $model_text = str_replace("//", 'protected $fillable = [\'name\'];'. "\n\t//", $model_text);
        file_put_contents($model_path, $model_text);
    }

    $migration_files = glob("./database/migrations/*". Str::plural($this->appName). "_table.php");

    if(count($migration_files) == 1)    //該当マイグレーションファイルが一つある場合
    {
        //migrationにnameカラム追加
        $migration_path = $migration_files[0];
        $migration_text = file_get_contents( $migration_path );
        $migration_text = str_replace(
            '$table->increments(\'id\');',
            '$table->increments(\'id\');'. "\n\t\t\t". '$table->text(\'name\');',
            $migration_text
        );
        file_put_contents($migration_path, $migration_text);
    }

クリエートモードのときの処理。 まずは、model作成です。 $this->call(‘artisan[コマンド]’, [‘アーギュメント’ => ‘[値]’]); で、artisan コマンドを呼出ししてます。 基本、ファイル操作をするときは、ファイルの有無を確認してから作業してます。 平行してデリートモードの処理も書いていってます。

コード書いては、クリエート⇒デリートを繰り返しテストしながら、すすめます。

/* ----------------------------------------
(2)ルーティング修正
-------------------------------------------*/
$routes_path = './app/Http/routes.php';
$routes_text = file_get_contents( $routes_path );
$routes_src = 'Route::resource(\''. Str::camel(Str::plural($this->appName)). '\', \''. Str::studly(Str::plural($this->appName)). 'Controller\');';

if( !stristr($routes_text, $routes_src) )    //ルート内に追加対象がない場合
{
    $routes_text = $routes_text. "\n". $routes_src;
    file_put_contents($routes_path, $routes_text);

    $this->info('routes rewrited successfully.');
}

次は、ルーティング 挿入箇所、ちょっと悩みましたが、最後に追加してます。

/* ----------------------------------------
(3)リクエスト作成
-------------------------------------------*/

$request_path = './app/Http/Requests/'. Str::studly($this->appName). 'Request.php';

if( !file_exists($request_path) )    //リクエストが無いとき
{
    $this->call('make:request', ['name' => Str::studly($this->appName). 'Request']);

    //Requestの修正
    $request_text = file_get_contents( $request_path );
    $request_text = str_replace(
        "public function authorize()\n\t{\n\t\treturn false;",
        "public function authorize()\n\t{\n\t\treturn true;",
        $request_text
    );
    $request_text = str_replace(
        "//",
         "'name' => 'required',". "\n\t//",
        $request_text
    );
    file_put_contents($request_path, $request_text);
}

リクエスト修正。
これも置換方法がいまいち・・・ artisanコマンドで生成されるファイルのフォーマットが変わったら、作り直しになりますね・・・。

/* ----------------------------------------
(4)ルートサービスプロバイダ修正
-------------------------------------------*/

$routes_service_provider_path = './app/Providers/RouteServiceProvider.php';
$routes_service_provider_text = file_get_contents( $routes_service_provider_path );
$routes_service_provider_src = '$router->model(\''. Str::camel(Str::plural($this->appName)). '\', \'App\\'. Str::studly($this->appName). '\');';


if( !stristr($routes_service_provider_text, $routes_service_provider_src) )    //ルート内に追加対象がない場合
{
    $routes_service_provider_text = str_replace(
        'parent::boot($router);'. "\n\t\t",
        'parent::boot($router);'. "\n\t\t$routes_service_provider_src\n\t\t",
        $routes_service_provider_text
    );
    file_put_contents($routes_service_provider_path, $routes_service_provider_text);

    $this->info('routes service provicer rewrited successfully.');
}

ルートサービスプロバイダで、モデルとルートを関連付けます。

/* ----------------------------------------
(5)コントローラー作成(テンプレートから)
-------------------------------------------*/

$controller_path = './app/Http/Controllers/'. Str::studly(Str::plural($this->appName)). 'Controller.php';
$controller_template_path = './app/Http/Controllers/MyAppTemplateController.php';

if( !file_exists($controller_path) )    //リクエストが無いとき
{
    $this->call('make:controller', ['name' => Str::studly(Str::plural($this->appName)). 'Controller']);

    //controllerの修正
    $controller_text = file_get_contents( $controller_template_path );
    $controller_text = str_replace(
        "[[[lc]]]",
        Str::camel($this->appName),
        $controller_text
    );
    $controller_text = str_replace(
        "[[[uc]]]",
        Str::studly($this->appName),
        $controller_text
    );
    $controller_text = str_replace(
        "[[[lcs]]]",
        Str::camel(Str::plural($this->appName)),
        $controller_text
    );
    $controller_text = str_replace(
        "[[[ucs]]]",
        Str::studly(Str::plural($this->appName)),
        $controller_text
    );
    file_put_contents($controller_path, $controller_text);
}

コントローラーは、
/app/Http/Controllers/MyAppTemplateController.php
に以下リプレイスをかけたもので上書きしてます。

[[[lc]]] ⇒ ローワーキャメルのアプリ名に。
[[[uc]]] ⇒ アッパーキャメルのアプリ名に。
[[[lcs]]] ⇒ ローワーキャメル(複数)のアプリ名に。
[[[ucs]]] ⇒ アッパーキャメル(複数)のアプリ名に。

/* ----------------------------------------
(6)ビュー作成(テンプレートから)
-------------------------------------------*/

$view_path = './resources/views/'. Str::camel(Str::plural($this->appName));
$view_template_path = './resources/views/myAppTemplate';
$file_names = array(
    'index' => 'index.blade.php',
    'show' => 'show.blade.php',
    'createAndEdit' => 'createAndEdit.blade.php',
);

if( !file_exists($view_path) )
{
    mkdir( $view_path, 0777, true );
    foreach ($file_names as $key => $value) {
        copy(
            $view_template_path. '/'. $value,
            $view_path. '/'. $value
        );

        $view_text = file_get_contents( $view_path. '/'. $value );
        $view_text = str_replace(
            "[[[lc]]]",
            Str::camel($this->appName),
            $view_text
        );
        $view_text = str_replace(
            "[[[uc]]]",
            Str::studly($this->appName),
            $view_text
        );
        $view_text = str_replace(
            "[[[lcs]]]",
            Str::camel(Str::plural($this->appName)),
            $view_text
        );
        $view_text = str_replace(
            "[[[ucs]]]",
            Str::studly(Str::plural($this->appName)),
            $view_text
        );
        file_put_contents($view_path. '/'. $value, $view_text);
    }
    $this->info('view file copied successfully.');
}

ビューも、コントローラー同様。
/resources/views/myAppTemplate をリプレイスして、コピーするだけです。

これで、スキャフォールドコマンド「myapp」が完成しました。 モデル制作手順としては、
(1)コンソールから php artisan myapp [アプリ名] -c
(2)マイグレーション、モデル、リクエストに、追加したいカラム項目を追加。
(3)マイグレーション php artisan migrate
で、シンプルなCRUDアプリの完成です。

いろいろ、改造の余地はあると思うので、 それぞれ、自分の好みを入れつつ、作ってみてください。 ではでは。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です