Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
15
Help us understand the problem. What is going on with this article?
@ayasamind

PHP8+ Laravel8 + laravel-generatorで簡単CRUD作成からユニットテストまで書く

この記事はFusic Advent Calenderの4日目の記事です。

昨日は@ryu022304さんのGo+serverlessでSQS→Lambda→SESの記事でした。

Serverless Framework便利ですよね。Goはまだあまりやったことないので、ちゃんと時間作ってやってみたいですね。


  

さて、この記事は、「PHP8+ Laravel8 + laravel-generatorで簡単CRUD作成からユニットテストまで書く」という記事です。

PHP8とLaravel8を触ってみたかったのと、Laravel-Generatorを案件で使ってみた知見の共有で、記事を書きます。

Laravel-Generatorはこちらですね。

InfyOmLabsというところが作っているみたいです。

一度案件で使ってみて、驚くほど一瞬でCRUD機能が作成できるので、早く動くものを作りたい時にはとても便利です。

最初の状態

Laravel8のプロジェクトをたてたところから始めます。ディレクトリ構成はこんな感じ。


├── README.md
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── coverage
├── database
├── docker-compose.yml
├── package.json
├── phpunit.xml
├── public
├── resources
├── routes
├── server
├── server.php
├── storage
├── tests
├── vendor
└── webpack.mix.js

Laravel8の最初の画面、初めてみました。
ちょっと暗い感じになりましたね。

Screen Shot 2020-12-03 at 16.34.47.png

PHPはphp:8.0-rc-fpmのDockerイメージを使いました。


$ php -v
PHP 8.0.0RC5 (cli) (built: Nov 25 2020 01:10:25) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies

Laravelのバージョンは


$ php artisan --version
Laravel Framework 8.17.0

8.17.0です。

Laravel-Generatorをインストールする

早速はじめていきます。

ドキュメント通りにインストール。


    "require": {
        "php": "^7.3|^8.0",
        "fideloper/proxy": "^4.4",
        "fruitcake/laravel-cors": "^2.0",
        "guzzlehttp/guzzle": "^7.0.1",
        "laravel/framework": "^8.12",
        "laravel/tinker": "^2.5",
        ↓以下の三つを追加↓
        "infyomlabs/laravel-generator": "8.0.x-dev", 
        "laravelcollective/html": "^6.2",
        "infyomlabs/coreui-templates": "8.0.x-dev"
    },

今回はAdminLTEではなく、CoreUIを使ってみます。


$ composer update

インストールに失敗。


  Problem 1
    - Root composer.json requires infyomlabs/laravel-generator 8.0.x-dev -> satisfiable by infyomlabs/laravel-generator[8.0.x-dev].
    - infyomlabs/laravel-generator 8.0.x-dev requires php ^7.3 -> your php version (8.0.0RC5) does not satisfy that requirement.
  Problem 2
    - Root composer.json requires infyomlabs/coreui-templates 8.0.x-dev -> satisfiable by infyomlabs/coreui-templates[8.0.x-dev].
    - infyomlabs/coreui-templates 8.0.x-dev requires php ^7.3 -> your php version (8.0.0RC5) does not satisfy that requirement.

Laravel-GeneratorがまたPHP8に対応していないみたいですねー。
きっと動作すると信じて、強引に入れてみますw

$ composer update --ignore-platform-reqs

インストール完了しました。

Laravel-Generatorの初期設定

app.phpにaliasを追記

こちらもドキュメント通り、app.phpにaliasを追加します。

        'Str' => Illuminate\Support\Str::class,
        'URL' => Illuminate\Support\Facades\URL::class,
        'Validator' => Illuminate\Support\Facades\Validator::class,
        'View' => Illuminate\Support\Facades\View::class,
        // 追加
        'Form'      => Collective\Html\FormFacade::class,
        'Html'      => Collective\Html\HtmlFacade::class,
        'Flash'     => Laracasts\Flash\Flash::class,

vendor:publishコマンドを実行

$ php artisan vendor:publish --provider="InfyOm\Generator\InfyOmGeneratorServiceProvider"

config/infyom/laravel_generator.php に設定ファイルが生成されます。

デフォルト設定だと、テンプレートがAdminLTEになってしまっているので、CoreUIに修正します。

   /*
    |--------------------------------------------------------------------------
    | Templates
    |--------------------------------------------------------------------------
    |
    */

    'templates'         => 'coreui-templates',

Laravel-GeneratorのPublishコマンドを実行

$ php artisan infyom:publish

Laravel UIで認証を作成する

CRUD作成の前に、Laravel6から使われているLaravelUIを用いて、簡単に認証機能を作成します。

ドキュメント通りコマンド実行。

$ composer require laravel/ui:^3.0 --ignore-platform-reqs
$ php artisan ui bootstrap --auth

この状態で、npm install && npm run devを実行すれば、ログイン画面が確認できますが、
今回は、Laravel-Genaratorのコマンドで上書きしてしまいます。

$ php artisan infyom.publish:layout --localized

上書きしますか?というメッセージが表示されますが、全て「y」を押して大丈夫です。

Login画面に行くと、CoreUIのログイン画面が表示されます。

Screen Shot 2020-12-03 at 17.28.47.png

多言語対応ができていないので、日本語ファイルを追加してあげましょう。

多言語対応

app.phpのlocaleの設定を日本語に変更。(ついでにTimezoneも変えときましょう)

    'timezone' => 'Asia/Tokyo',
    'locale' => 'ja',

resources/lang/ja配下に日本語ファイルを設置します。

auth.php
<?php

return [
    'failed' => 'メールアドレス、もしくはパスワードが違います',
    'throttle' => 'ログイン試行回数がしきい値を超えました。 時間をおいてログインしてください。',
    'login' => 'ログインしました',
    'logout' => 'ログアウトしました',
    'notVerified' => 'メールが認証されていません',
    'verified' => 'メールの認証に成功しました',
    'alreadyVerified' => '既に認証済みのメールアドレスです',
    'sendVerifyMail' => '認証メールを再送しました',
    'noUser' => '入力されたメールアドレスのユーザーが存在しません',

    'email'            => 'メールアドレス',
    'password'         => 'パスワード',
    'confirm_password' => 'パスワードを確認',
    'sign_in'          => 'ログイン',
    'admin_sign_in'    => '管理者ログイン',
    'sign_out'         => 'ログアウト',
    'register'         => 'ユーザー登録',
    'full_name'        => '氏名',

    'login' => [
        'forgot_password'     => 'パスワードを忘れた時',
    ],

    'forgot_password' => [
        'send_pwd_reset' => 'パスワード変更リンクを送る',
    ],

    'reset_password' => [
        'title'         => 'パスワードを変更',
        'enter_email' => 'メールアドレスを入力してください'
    ],

    'registration' => [
        'have_membership' => '既にアカウントを持っている方はこちら'
    ]

];

必要なさそうな文言はresources/views/auth/login.blade.phpresources/views/auth/register.blade.phpを削って対応。

以下のように表示されました。

Screen Shot 2020-12-03 at 17.41.46.png

ユーザー登録・ログイン

マイグレーションを走らせれば、ユーザー登録、ログインができます。

$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (117.26ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (84.44ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (205.19ms)

ログインした後はこんな感じの画面に。

Screen Shot 2020-12-03 at 17.43.32.png

まっさら何もないですね。

ここから本題のCRUDを作成していきます。

Laravel-GeneratorでCRUDを作成する

今回は簡単な投稿をつくるためのCRUDを作ってみます。

Laravel-Generatorのコマンドを実行。

Model名ArticleのCRUDを作成します。

$ php artisan infyom:scaffold Article
Specify fields for the model (skip id & timestamp fields, we will add it automatically)
Read docs carefully to specify field inputs)
Enter "exit" to finish

 Field: (name db_type html_type options) []:
 >

カラムのDBタイプとHTMLタイプを聞かれるので、答えます。その後Validationも聞かれます。

HTMLタイプの候補に関しては、Laravel-Generatorのサイトに書いてあります。
http://infyom.com/open-source/laravelgenerator/docs/8.0/fields-input-guide

Validationの書き方についてはこちらに。
http://infyom.com/open-source/laravelgenerator/docs/8.0/getting-started#validations

今回は以下のような感じでやってみました。

 Field: (name db_type html_type options) []:
 > title string text

 Enter validations:  []:
 > required|max:20

 Field: (name db_type html_type options) []:
 > body text text

 Enter validations:  []:
 > required

 Field: (name db_type html_type options) []:
 > user_id integer:unsigned:foreign,users,id select

 Enter validations:  []:
 > required|numeric

 Field: (name db_type html_type options) []:
 > exit

自動でMigrationファイルやRepository、Model、Controller、Viewなどが作成されます。

Migration created:
2020_12_03_180423_create_articles_table.php

Model created:
Article.php

Repository created:
ArticleRepository.php

Factory created:
ArticleFactory.php

Create Request created:
CreateArticleRequest.php

Update Request created:
UpdateArticleRequest.php

Controller created:
ArticleController.php

Generating Views...
table.blade.php created
index.blade.php created
field.blade.php created
create.blade.php created
edit.blade.php created
show_fields.blade.php created
show.blade.php created

Migrationの実行を聞かれて、実行してみましたが、失敗。。
UsersのMigrationファイルの問題みたいだったので、修正します。

2014_10_12_000000_create_users_table
<?php

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

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

マイグレーションを再度実行

$ php artisan migrate:refresh
Rolling back: 2020_12_03_180423_create_articles_table
Rolled back:  2020_12_03_180423_create_articles_table (125.45ms)
Rolling back: 2019_08_19_000000_create_failed_jobs_table
Rolled back:  2019_08_19_000000_create_failed_jobs_table (34.11ms)
Rolling back: 2014_10_12_100000_create_password_resets_table
Rolled back:  2014_10_12_100000_create_password_resets_table (33.85ms)
Rolling back: 2014_10_12_000000_create_users_table
Rolled back:  2014_10_12_000000_create_users_table (33.50ms)
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (85.98ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (93.14ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (126.64ms)
Migrating: 2020_12_03_180423_create_articles_table
Migrated:  2020_12_03_180423_create_articles_table (198.16ms)

うまくいきました。

ログインユーザーのみが記事確認、作成できるようにする

生成されたRouteのままだと、ArticleのResourceがAuth Middlewareの外にあるので、Routeをいじってあげます。

web.php
Route::middleware('auth')->group(function () {
    Route::resource('articles', App\Http\Controllers\ArticleController::class);
});

画面を確認

Article一覧画面ができてます。

Screen Shot 2020-12-03 at 18.16.58.png

記事の作成、編集、削除もできますね。

コマンド一つでCRUDが作成できました。

ユニットテストを書く

最後にユニットテストを書いていきます。

まずは何もしない状態でテストを実行してみます。Laravelのデフォルトのテストが実行されます。

$ ./vendor/bin/phpunit
PHPUnit 9.4.4 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:01.291, Memory: 20.00 MB

OK (2 tests, 2 assertions)

ArticleRepositoryのテストを書く

記事のCRUDが動いていることを確認するテストを書きます。
せっかくPHP8を使っているので、Repositoryのメソッドの引数には、無駄に名前付き引数を使ってみました。

ArticleRepositoryTest
<?php

namespace Tests\Unit\Repositories;

use App\Models\Article;
use App\Models\User;
use App\Repositories\ArticleRepository;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ArticleRepositoryTest extends TestCase
{
    use RefreshDatabase;

    private $articleRepository;

    public function setUp(): void
    {
        parent::setUp();
        $this->articleRepository = app(ArticleRepository::class);
    }

    /**
     * 記事全取得のテスト
     * @return void
     */
    public function testPaginate(): void
    {
        // Laravel8以降、Globalなfactory関数は廃止され、Modelから呼び出す形となりました
        Article::factory(10)->create();
        $articles = $this->articleRepository->paginate(perPage: 10);;
        $this->assertInstanceOf(Collection::class, $articles->getCollection());
        $this->assertCount(10, $articles);
    }

    /**
     * 記事一件取得のテスト
     * @return void
     */
    public function testGetById(): void
    {
        $articleId = 1;
        Article::factory()->create([
            'id' => $articleId
        ]);
        $article = $this->articleRepository->find(id: $articleId);
        $this->assertInstanceOf(Article::class, $article);
    }

    /**
     * 記事作成編集削除のテスト
     * @return void
     */
    public function testCreateAndUpdateAndDelete(): void
    {
        // Create
        $data = [
            'title'   => 'タイトル',
            'body'    => '内容',
            'user_id' => User::factory()->create()->id,
        ];
        $createdArticle = $this->articleRepository->create(input: $data);
        $this->assertInstanceOf(Article::class, $createdArticle);
        $this->assertEquals($data['title'], $createdArticle->title);

        // Update
        $updated = [
            'title'   => '更新されたタイトル',
        ];
        $updatedArticle = $this->articleRepository->update(input: $updated, id: $createdArticle->id);
        $this->assertInstanceOf(Article::class, $updatedArticle);
        $this->assertEquals($updatedArticle['title'], $updatedArticle->title);

        // Delete
        $this->articleRepository->delete(id: $updatedArticle->id);
        $this->assertNull($this->articleRepository->find($updatedArticle->id));
    }
}

factoryをModelから呼び出すところはLaravel8で変わった点ですね。

テスト実行

test/Repositories配下を実行するように、phpunit.xmlを修正してあげます。

phpunit.xml
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
        ↓追加
        <testsuite name="Repositories">
            <directory suffix="Test.php">./tests/Repositories</directory>
        </testsuite>

テストを実行します。

$ ./vendor/bin/phpunit
PHPUnit 9.4.4 by Sebastian Bergmann and contributors.

.....                                                               5 / 5 (100%)

Time: 00:02.388, Memory: 30.00 MB

OK (5 tests, 10 assertions)

通りましたね。

権限のテストを書く

ログインしたユーザーだけが記事一覧や、記事作成ができる形になっているかをテストします。

AuthTest.php
<?php

namespace Tests\Unit;

use App\Models\Article;
use App\Models\User;
use Auth;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AuthTest extends TestCase
{
    use RefreshDatabase;

    private Article $article;

    public function setUp(): void
    {
        parent::setUp();
        $this->article = Article::factory()->create();
    }

    /**
     * 記事へのアクセステスト
     *
     * @return void
     */
    public function testArticleAccess(): void
    {
        // ログイン前はログイン画面にリダイレクトされる
        $this->get(route('articles.index'))->assertRedirect(route('login'));
        $this->get(route('articles.create'))->assertRedirect(route('login'));
        $this->get(route('articles.show', $this->article))->assertRedirect(route('login'));
        $this->get(route('articles.edit', $this->article))->assertRedirect(route('login'));

        $user = $this->actingAs(
            User::factory()->create()
        );

        // ログイン後は正常にアクセス可能
        $user->get(route('articles.index'))->assertStatus(200);
        $user->get(route('articles.create'))->assertStatus(200);
        $user->get(route('articles.show', $this->article))->assertStatus(200);
        $user->get(route('articles.edit', $this->article))->assertStatus(200);

        Auth::logout();

        // ログアウト後はログイン画面にリダイレクトされる
        $this->get(route('articles.index'))->assertRedirect(route('login'));
        $this->get(route('articles.create'))->assertRedirect(route('login'));
        $this->get(route('articles.show', $this->article))->assertRedirect(route('login'));
        $this->get(route('articles.edit', $this->article))->assertRedirect(route('login'));
    }
}

実行してみます。

$ ./vendor/bin/phpunit
PHPUnit 9.4.4 by Sebastian Bergmann and contributors.

......                                                              6 / 6 (100%)

Time: 00:02.842, Memory: 30.00 MB

OK (6 tests, 30 assertions)

無事通りましたね!
実行対象のテストを絞りたい際は

$ ./vendor/bin/phpunit --filter='AuthTest'

のような形で絞ることも可能です。

長くなりましたが、これでLaravel-Generatorを使って簡単にCRUDを作成し、ユニットテストまで書くことができました。

PHP8やLaravel8は今回初めて使いましたが、もっといろんな機能があるので、早く色々試してみたいですね。

最後まで読んでいただき、ありがとうございました!!

明日は @gorogoroyasu の記事ですね。機械学習系の記事かな?楽しみです!

15
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
15
Help us understand the problem. What is going on with this article?