20191213のlaravelに関する記事は13件です。

Laravel の認可(Policy)にモデル以外のパラメータを与える

この記事について

Laravel #2 Advent Calendar 2019 - Qiita 14日目の記事です。

表題の通りではあるんですが、意外と使われてないなぁと思ったので記事にすることにしました。

はじめに

環境

  • Laravel 6.6.0

5.x でも同じです。

Policy について

特定のモデルに対し、ユーザーが操作権限があるかどうかを判定する仕組みで、公式ドキュメントには以下のような使用例が載っています。

class PostPolicy {
    public function update(User $user, Post $post) {
        return $user->id === $post->user_id;
    }
}

呼び出し側は、

class PostController extends Controller {
    public function update(Request $request, Post $post) {
        $this->authorize('update', $post);
    }
}

みたいになります。

この仕組みを利用するメリットは、

  1. 渡されたモデルのインスタンスに応じて Policy クラスをよしなに選択してくれる
  2. User インスタンスを自動でバインドしてくれる(デフォルトでは現在ログインしているユーザー)
  3. 権限がない場合は勝手に 403 エラーにしてくれる

あたりでしょうか。

権限管理が複雑なアプリケーションだと、アプリケーション独自で認可機構を実装したほうが柔軟にできると思うので、無理に使う必要はないと思いますが、そこはチームのポリシー次第、ということで。

ユースケース

  1. 他のモデルの状態が必要
  2. 静的な権限テーブルみたいなやつがある
  3. 動的にポリシーを差し替えたい

ざっと思いついたものを挙げましたが、基本的にはぜんぶ一緒です。

結論

authorize メソッドに配列で引数を渡すと、Policy の各メソッドには、スプレッド演算子によって展開された形で渡ってくるので、それらを使って複雑な認可ルールに対処できます。

呼び出し側でこのように呼ぶと、

$this->authorize('update', [$mainModel, $subModel]);

Policy のメソッドにはこのように渡ってきます。

public function update(User $user, MainModel $main, SubModel $sub) {

使用例

1. 他のモデルの状態が必要

ユーザーにも対象のモデルにもひもづかない、別のモデルの状態が必要になったとき、Controller でいったんそのモデルのインスタンスを取得して、authorize メソッドに渡してやります(ちょっといい例が浮かばなかったので適当なモデル名になっていますが、ご容赦ください)。

public function update(Request $request, Post $post) {
    $someModel = SomeModel::findOrFail($request->some_model_id);
    $this->authorize('update', [$post, $someModel]);
}

Policy 側では以下のように状態を参照できます(メソッドを呼んでもいいでしょう)。

public function update(User $user, Post $post, SomeModel $someModel) {
    return $user->id === $post->user_id && $someModel->acceptable;
}

2. 静的な権限テーブルみたいなやつがある

たとえばユースケースごとに操作可能な権限のリストを持っていて、それに合致しない場合は弾く、みたいなケースです。

ユースケースに決め打ちで(もしくはなんらかのルールに基づいてデータベースから取ってくるとかでも)、

class UpdateTask extends UseCase {
    public function validRoles(): array {
        return [Role::Admin, Role::Wheel];
    }
    public function invoke() {...}
}

みたいに権限のリストがあり、それを以下のように authorize に渡してやります。

public function update(UpdateTask $useCase, Post $post) {
    $this->authorize('update', [$post, $useCase->validRoles()]);
}

Policy 側はこうなります。

public function update(User $user, Post $post, array $roles) {
    return $user->hasRole($roles) || $user->id === $post->user_id;
}

3. 動的にポリシーを差し替えたい

ユースケースごとに独自で認可ルール(関数)を持っていて、そいつをなんらかのルールで動的に生成する、といったようなケースです。

class UpdateTask extends UseCase {
    public function authorizeRule(): \Closure {
        return function (User $user, Task $task): bool {
            // ...
        };
    }
    public function invoke() {...}
}

呼び出し側は、

public function update(UpdateTask $useCase, Post $post) {
    $this->authorize('update', [$post, $useCase->authorizeRule()]);
}

で、Policy 側はこうなります。

public function update(User $user, Post $post, \Closure $rule) {
    return $rule($user, $task);
}

ここまでくると、Policy 使う必要ない気もしてきますが、認可の仕組み自体は Laravel に任せて、ドメインロジックはドメインモデルに任せる、という分担もやりやすいと思うので、検討する価値はあるんじゃないかと思います。

おわりに

上記に挙げた以外にもユースケースがありましたら、コメント欄にて教えていただけると助かります :bow:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】保守性をアップさせるかもしれない 3 つのテクニック

はじめに

初めまして、MasaKu です。

今年も残すところわずかですね。

今年もたくさんのコードを書いてきたことだと思います。

中には埃をかぶりまくった個人開発のコードもあるかと思いますが、年末のこの時期にちょっとだけリファクタリングとして手を入れていみませんか?

今回は、Laravel で作成されたアプリケーションを、ちょこっとした工夫だけで可読性をアップできるかもしれないテクニックをご紹介します。

その1:ルートグループ

Laravel では web.php にルーティングする処理を記載します。

超ざっくりしたブログページのサンプルを作成しました。

// ホーム画面
Route::get('/home', 'HomeController@home');

// ブログページ
Route::get('/blog', 'BlogController@blog');
Route::get('/create', 'BlogController@create');
Route::get('/update', 'BlogController@update');
Route::get('/delete', 'BlogController@delete');

この時、ブログページにアクセスする際は、twitter の OAuth でログイン認証している状態にさせたいという場合は各ルーティングにミドルウェアを設定をするのが便利かと思います。

// ブログページ
Route::get('/blog', 'BlogController@blog')->middleware('oAuth');
Route::get('/create', 'BlogController@create')->middleware('oAuth');
Route::get('/update', 'BlogController@update')->middleware('oAuth');
Route::get('/delete', 'BlogController@delete')->middleware('oAuth');

もちろん、このように1件ずつ Middleware を設定することもできるのですが、これだと Middleware を差し替えようとした際に、全ての Middleware の定義を修正しなければならなくなります。

それに、こういったコードがたくさん並ぶとパッと見たときに、どの Middleware を設定しているのかがわかりづらいし、Middleware を付け忘れてしまうことにもなりかねません。

こういう時は、ルーティングをグループ化して、そのグループに Middleware を設定するとコードがスッキリしてわかりやすくなります。

// ブログページ
Route::middleware(['oAuth'])->group(function(){
    Route::get('/blog', 'BlogController@blog');
    Route::get('/create', 'BlogController@create');
    Route::get('/update', 'BlogController@update');
    Route::get('/delete', 'BlogController@delete');
});

このようにすれば、パッと見ただけで Middleware をかける処理が一覧として理解しやすく付け忘れるということもなくなると思います。

ちなみに、グループ化しておくことのメリットとして、グループ内のコントローラ対して名前空間を指定することもできるということもあります。

今後コントローラの名前空間を変更した場合なども一括で設定することができるので非常に便利です。

// ブログページ
Route::namespace('Blog')->middleware(['oAuth'])->group(function(){
    Route::get('/blog', 'BlogController@blog');
    Route::get('/create', 'BlogController@create');
    Route::get('/update', 'BlogController@update');
    Route::get('/delete', 'BlogController@delete');
});

その2:サービスコンテナからサービスクラスを利用する

Laravel ではコントローラ内に処理を記載して、処理結果を View に渡すようにしていきます。

そのため、コントローラ内が複雑になってしまうことをできるだけ避けたいのではないでしょうか。

特に、似たような処理がほかのコントローラ内にあるような状態はできるだけ避けたいです。

そんなときは、共通処理をサービスクラスに書き出してサービスコンテナに登録し、コントローラ内でそのクラスのインスタンスを利用するようにしましょう。

Laravel プロジェクトの app フォルダ内に OriginClasses というフォルダを作成します。

その後、OriginService.php というファイルを作成し、以下のように入力してください。

<?php


namespace App\OriginClasses;


class OriginService
{
    private $msg;
    private $data;

    public function __construct()
    {
        $this->msg = 'サービスクラスを取得';
        $this->data = ['ホーム', 'ブログ'];
    }

    public function getMessage()
    {
        return $this->msg;
    }

    public function getData()
    {
        return $this->data;
    }
}

メッセージとデータを取得する共通処理です。

この処理を各コントローラ内で呼び出して利用できるようにしていきます。

Laravel では app()というメソッドを通してサービスコンテナからインスタンスを取得することができます。

以下のようにコントローラ内に app('App\OriginClasses\OriginService') を記載することで、インスタンスを取得することができます。

public function blog()
{
    // OriginServiceクラスのインスタンスを取得
    $originService = app('App\OriginClasses\OriginService');

    echo $originService->getMessage();
    return view('blog');
}

このようにしておくことで、コントローラ内に共通の処理がいたるところにベタ書きされることを防ぐことができます。

今後、同じ処理を別のコントローラに埋め込みたくなったときや、これまで定義していた共通処理を修正する場合も、クラスファイルのメソッドを修正するだけですべてのコントローラ内の処理を修正することができるので非常に便利です。

その3:ファサードを使う

先程はサービスコンテナからインスタンスを取得する方法をご紹介しましたが、サービスを利用する入口だけを使ってサービスを利用する方法もがります。

ファサードではコントローラ側でインスタンスを取得するのではなく、サービスの入口だけを利用して処理することができます。

まずは、Laravel プロジェクト内のappフォルダにFacadesというフォルダを作成します。

その後、Facadesフォルダ内にOriginService.phpというファイルを作成して以下のように入力してください。

<?php

namespace App\Facades;
use Illumination\Support\Facades\Facade;

class OriginService extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'originservice';
    }
}

それでは、作成したファサードを/config/app.phpaliases配列に追加していきます。

    'aliases' => [

        'App' => Illuminate\Support\Facades\App::class,
        'Arr' => Illuminate\Support\Arr::class,

        // 一部省略

        'Validator' => Illuminate\Support\Facades\Validator::class,
        'View' => Illuminate\Support\Facades\View::class,
        'originservice' => App\Facades\MyService::class, //この行を追加

    ],

最後にaliasesに登録したファサードを Laravel のサービスプロバイダに設定して利用できるようにしましょう。

サービスプロバイダは以下のコマンドで作成できます。

php artisan make:provide OriginServiceProvider

作成したOriginServiceProvider.phpを以下の通り修正してください。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class OriginServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        app()->singleton('originservice',
            'App\OriginClasses\OriginSevice');
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

作成したサービスプロバイダを /config/app.php に設定します。

    'providers' => [
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,

        // 一部省略

        App\Providers\RouteServiceProvider::class,
        App\Providers\OriginServiceProvider::class, //この行を追加

    ]

これでファサードを通してサービスを利用できるようになりました。

あとは以下のようにすることでコントローラ側からサービスを利用することができます。

use App\Facades\OriginService;

class BlogController extends Controller
{
    public function blog()
    {
        OriginService::getMessage();
        return view('blog');
    }

ファサードの処理の流れを整理しますと以下のとおりになります

  1. ファサードを作成し getFacadeAccessor() メソッドに利用するサービスのファサード(入口)の名前を返すようにする
  2. config/app.php にファサードを登録する(エイリアスとファサードの実態を登録)
  3. サービスプロバイダを作成し、利用するサービス(コントローラ側で呼び出したい処理)をファサードの名前で設定する
  4. コントローラ側でファサードを経由してサービスの処理を実行する

おわりに

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

いずれも基本的な内容にはなりますが、もし取り入れていなくてもちょっとした工夫だけで直ぐに取り入れることができる内容なので、リファクタリングとしてちょうど良い内容なのではないかと思いました。

ぜひお試しあれ!

参考資料

PHP フレームワーク Laravel 実践開発

Laravel 5.7 ミドルウェア

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel Seeding で Users, Posts, Comments テーブルの値を埋める方法

Laravel の Seeding 機能を利用して、users, posts, comments テーブルの値を埋める方法を書きます。

各テーブルのカラム

各テーブルのカラムは以下の通りです。

users posts comments
id id id
name user_id user_id
email title post_id
email_verified_at text text
password created_at created_at
remember_token updated_at updated_at
created_at
updated_at

手順

UsersTableSeeder を作成する

これは UserFactory を実行するために使うファイル(クラス)です。

php artisan make:seeder UsersTableSeeder
database/seeds/UsersTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    public static $nbPosts = 0;

    public static function count($nb) {
        self::$nbPosts += $nb;

        //echo self::$nbUsers."\n";
    }

    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(App\User::class, 50)->create();
    }
}

PostsTableSeeder を作成する

今回はこのファイル(クラス)は使いませんが、一応、作っておきます。

php artisan make:seeder PostsTableSeeder

CommentsTableSeeder を作成する

これは comments テーブルの post_id にランダムな値を入れるためのファイルです。

php artisan make:seeder CommentsTableSeeder
database/seeds/CommentsTableSeeder.php
<?php

use Illuminate\Database\Seeder;

use Faker\Generator as Faker;

use UsersTableSeeder as Start;

class CommentsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run(Faker $faker)
    {
        $comments = App\Comment::all();

        foreach($comments as $comment) {
            $comment->post_id = $faker->numberBetween(1, Start::$nbPosts);

            $comment->save();
        }
    }
}

DatabaseSeeder に上記の3つの Seeder を設置する

DatabaseSeedercall メソッドに名称を渡すことで、ユーザー自作の Seeder ファイルを実行します。

database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            UsersTableSeeder::class,
            PostsTableSeeder::class,
            CommentsTableSeeder::class
        ]);
    }
}

UserFactory を作成します

User モデルに対応するテーブル(= users )の「行」を自動的に作成する際の参考となるファイルです。

php artisan make:factory UserFactory --model=User
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;

use UsersTableSeeder as Seeder;

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

$factory->afterCreating(User::class, function ($user, $faker) {
    $nb = $faker->numberBetween(1, 3);

    Seeder::count($nb);

    $user->posts()->createMany(factory(App\Post::class, $nb)->make()->toArray());

    $nb = $faker->numberBetween(4, 6);

    $user->comments()->createMany(factory(App\Comment::class, $nb)->make()->toArray());
});

PostsFactory を作成します

Post モデルに対応するテーブル(= posts )の「行」を自動的に作成する際の参考となるファイルです。

php artisan make:factory PostFactory --model=Post
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Post;
use Faker\Generator as Faker;

$factory->define(Post::class, function (Faker $faker) {
    return [
        'title' => $faker->words(4, true),
        'text' => $faker->paragraph(1),
        'user_id' => 1
    ];
});

CommentFactory を作成します

Comment モデルに対応するテーブル(= comments )の「行」を自動的に作成する際の参考となるファイルです。

php artisan make:factory CommentFactory --model=Comment
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Comment;
use Faker\Generator as Faker;

$factory->define(Comment::class, function (Faker $faker) {
    return [
        'text' => $faker->paragraph(1),
        'user_id' => 1,
        'post_id' => 1
    ];
});

autoload ファイルを作成します

私にはこの役割がよくわかりませんが、これも Seeding に必要だそうです。

composer dump-autoload

Seeding を実行する

データベースのデータをすべて消去した上で、Seeding を実行します。

php artisan migrate:fresh --seed

補足

user_id

posts テーブルと comments テーブルの user-id カラムを afterCreating メソッドのコールバック関数で埋めています。

<?php

$factory->afterCreating(User::class, $cb);

上記のようにすると、User モデルを作成するたびに、その直後に、$cb を実行できます。

今回の場合は、この $cb の中で User モデルの postscomments メソッド( Relationship )を使って user-id カラムを埋めています。

post_id

上記のように afterCreating メソッドを利用するだけでは、comments テーブルの post_id カラムにランダムな値を埋めることができません。

今回の場合は、PostFactory で作成した posts テーブルの行数を UsersTableSeeder で記録し、それを参考に CommentsTableSeedercomments テーブルの各行の post_id の値を記録した行数以下の値の整数に更新しています。

感想

UsersTableSeederusers テーブルの行だけを作成し、作成した行数をそのクラスの static property として記録し、PostsTableSeederCommentsTableSeeder でも同様にするほうが柔軟に開発できるまもしれません…。

参考

https://laravel.com/docs/6.x/seeding

https://laravel.com/docs/6.x/database-testing#writing-factories

https://laravel.com/docs/6.x/eloquent#retrieving-models:title

https://www.php.net/manual/en/language.oop5.static.php

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フロントVue.js、サーバーサイドLaravelの実装方法

概要

1つのアプリを、フロントはVue.js、サーバーサイドはLaravelで作成し、Vue.js側でサーバーサイドをAPIとして引っ張ってきて使用したい。

axios

store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

const URL_BASE = '全ページ共通のホスト部分URL';
var token = "生成されたトークン";

//新しいVuexを生成
export default new Vuex.Store({
  state: {
    list: []
  },
  actions: {
    //このcontextがないと、urlが上手く渡って来ないことがある。
    get_ajax(context, url) {
      //変数urlはviewで定義する
      return axios.get(URL_BASE + url, {    
         //このheadersは、認証が必要なページの時に記載する。
          headers: {
            "Content-Type": "application/json",
            "Authorization": 'Bearer ' + token
          },
          responseType: 'json',
        })
        //resに送られてきたデータが入っている。
        .then((res) => {
          //下記の記載で、ここの配列listにres.dataをsetしている。
          Vue.set(this, 'list', res.data);
        });
    },
  }
});
views/index.js
//ここら辺は今回関係ないとこ
<template>
  <div>
    <Template/>
  </div>
</template>


<script>
  //これは今回関係なし。
  import Template from "@/common/Template.vue"
  //vuexのmapActionsが使えるようになる。
  import { mapActions } from 'vuex'

  var url = 'このサーバーサイドで実装した、このviewページのurl';

  export default {
    data() {
      return {
        list: []
      }
    },
    //ここら辺も今回関係なし
    components: {
      Template
    },
    //mapActionsにstoreで定義したget_ajaxが使えるように記載する。
    methods: {
      ...mapActions([
        'get_ajax'
      ]),
    },
    //asyncとawaitで非同期通信となる。
    async mounted() {
      await this.get_ajax(url).then(()=>{
        //ここの記載でdataの中の配列listにstoreで定義されたlistを代入している。
        this.list = this.$store.list;
      });
    }
  }
</script>

参考資料
https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelのAPIのテストにおいて、APIのリクエスト手法を間違えてテストに失敗した話

Laravelの更新系APIのテストにおいて、APIのリクエスト手法を間違えたばかりにテストが失敗して悩んだ話を書きます。
ちゃんと公式ドキュメントを読めば当たり前のことなのですが、APIのテストにはjsonメソッドを使いましょう。

APIに関連付けられたフォームリクエストが実行されるとき、
例えばformタグをsubmitして、バリデーションエラーが発生したら入力画面にリダイレクトしてほしい。
また、AJAXのAPIとしてリクエストし、バリデーションエラーならばJSONでエラーメッセージを返してほしい。
HTTPステータスコードでいうと、リダイレクトは302、バリデーションエラーのJSONレスポンスは422となります。
Laravelでバリデーションするときに使われるフォームリクエストは、この辺りの処理を自動でやってくれます。ありがたい:pray:
公式ドキュメントにもちゃんと書かれています。

If the request was an AJAX request, a HTTP response with a 422 status code will be returned to the user including a JSON representation of the validation errors.

https://laravel.com/docs/6.x/validation#form-request-validation

そのHTTPステータスコードの切り替えに関わるAPIの呼び出し方をテストで再現しなかったばかりにテストが失敗して悩んでしまいました。
再現するために、ちゃちゃっとLaravelで更新系のAPIを作ってみます。使うLaravelのバージョンは6.xです。

$ composer create-project --prefer-dist laravel/laravel apisample
$ cd apisample
$ php artisan make:request UpdateUserNameRequest
$ php artisan make:controller UpdateUserNameApi
$ php artisan make:test UpdateUserNameApiTest
routes/api.php
<?php

use Illuminate\Http\Request;

Route::patch('/update_user_name', 'UpdateUserNameApi');
app/Http/Requests/UpdateUserNameRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserNameRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required',
        ];
    }
}

名前は必須だよというフォームリクエストです。

app/Http/Controllers/UpdateUserNameApi.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\UpdateUserNameRequest;

class UpdateUserNameApi extends Controller
{
    public function __invoke(UpdateUserNameRequest $request)
    {
        // 更新処理

        return response()->json(['message' => '更新完了']);
    }
}

とりあえずの更新APIなので、どのユーザの名前を更新するんやい!! という点は置いといてください:pray:

次にテストです。まずは私が最初に書いて失敗したテストです。

tests/Feature/UpdateUserNameApiTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class UpdateUserNameApiTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testApi()
    {
        // エラー
        $this->patch('/api/update_user_name', [])->assertStatus(422);

        // 成功
        $this->patch('/api/update_user_name', ['name' => 'Takashi'])->assertStatus(200);
    }
}

patchでリクエストして、エラーだと422を想定してassertStatus(422)してます。
これを実行すると、

$ vendor/bin/phpunit tests/Feature/UpdateUserNameApiTest.php 
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 383 ms, Memory: 18.00 MB

There was 1 failure:

1) Tests\Feature\UpdateUserNameApiTest::testApi
Expected status code 422 but received 302.
Failed asserting that false is true.

apisample/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:183
apisample/tests/Feature/UpdateUserNameApiTest.php:19

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

422じゃなくて302だよと言われ、テストが失敗してしまいました。

よくよく考えると、このテストで実行しているリクエストはAJAXリクエストではありません。
なので422ではなくて、リダイレクトの302が返ってくるわけです。

それじゃあ、実際の運用と同じようにAPIにリクエストするにはどうしたらよいかというと、公式ドキュメントにしっかり載っていました。

https://laravel.com/docs/6.x/http-tests#testing-json-apis

tests/Feature/UpdateUserNameApiTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class UpdateUserNameApiTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testApi()
    {
        // エラー
        $this->patchJson('/api/update_user_name', [])->assertStatus(422);

        // 成功
        $this->patchJson('/api/update_user_name', ['name' => 'Takashi'])->assertStatus(200);
    }
}
$ vendor/bin/phpunit tests/Feature/UpdateUserNameApiTest.php 
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 112 ms, Memory: 18.00 MB

OK (1 test, 2 assertions)

公式ドキュメントをちゃんと読みましょう自分:innocent:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Key Vault を利用した .env 内の機密情報の管理

.env と機密情報

Laravel を利用したプロジェクトは通常 .env を使って環境変数を管理しています。.env には各種認証情報などを含めることもあると思いますが、それらは Git などのバージョン管理システムに平文で保存するべきではありません。
かと言って、どこにも管理されておらず稼動している環境に置いてあるだけの状態というのも心許無さがあります。

そこで Azure Key Vault (和名:キー コンテナー)のシークレットと、Go で書いた簡素な vaultenv というツールで、 .env に直接機密情報を記述することなく管理できるようにしました。以下では Azure VM を利用していることを前提に、例を紹介します。

Azure Key Vault の準備

  • まずは Key Vault を作成します。(ここでは Azure CLI を使った例で説明します)
az keyvault create --location japaneast --name <YourKeyVaultName> --resource-group <YourResourceGroupName>
  • 開発者のグループやユーザーにセット(set)と一覧取得(list)の権限を付与します。格納されているデータは取得できないようにします。
az keyvault set-policy --resource-group <YourResourceGroupName> --name <YourKeyVaultName> --secret-permissions set list --object-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  • シークレットを復元したいデプロイ用途などの VM に対して、取得の権限を付与します。
az vm identity assign --name <NameOfYourVirtualMachine> --resource-group <YourResourceGroupName>
{
  "systemAssignedIdentity": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "userAssignedIdentities": {}
}
az keyvault set-policy --name <YourKeyVaultName> --object-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --secret-permissions get

Key Vault へ機密情報の格納

  • 機密情報を Key Vault のシークレットに保存します。
$ az keyvault secret set --vault-name <YourKeyVaultName> -n example-password --value "naisho"
{
  "attributes": {
    "created": "2019-12-13T03:41:15+00:00",
    "enabled": true,
    "expires": null,
    "notBefore": null,
    "recoveryLevel": "Purgeable",
    "updated": "2019-12-13T03:41:15+00:00"
  },
  "contentType": null,
  "id": "https://<YourKeyVaultName>.vault.azure.net/secrets/example-password/97a8cfac350c4b67b1f3510b1598cdce",
  "kid": null,
  "managed": null,
  "tags": {
    "file-encoding": "utf-8"
  },
  "value": "naisho"
}
  • 登録時に出力された id を {{ kv < id > }} の形式で任意のテキストファイルに埋め込みます。example-password 以下(/97a8c...) も含めるとバージョンを固定することができます。含めない場合は最新の値を取得します。
.env.template
USER=user1
PASSWORD={{ kv "https://<YourKeyVaultName>.vault.azure.net/secrets/example-password" }}

.env への展開

  • シークレットの取得を許可した VM 上で行います。上記のファイルを vaultenv を通すことにより {{ kv < id > }} で記述された部分が Key Vault に保存したデータに置換されます。実際にはデプロイ時に自動実行されるスクリプト内で行っています。
$ go get github.com/sensyn-robotics/vaultenv
$ vaultenv < .env.template > .env
$ cat .env
USER=user1
PASSWORD=naisho

まとめ

Key Vault のアクセスポリシーを設定するすることにより、開発者自身のアカウントでは保存されたデータを参照せずに、登録のみが行えるようにすることができます。さらに、データの取得権限を特定の VM に限定することで、(VM に対するアクセスコントロールを適切に行えていれば)機密情報が漏洩してしまうリスクを低減できます。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

php-fpmで動くLaravelのdocker imageをgithub actions使ってECRへデプロイ

laravelのアプリをphp-fpmでsocket使ってgithub actionsでECSにデプロイしたのでメモ。
php&laravel知識は私にはほぼないです。DB使ってないアプリなのでDB設定なし。

雑において使えればよかったのでいろいろ適当です。
だったらphp-fpmにする必要もなかったんですができるのかなと思ってやったら出来たので記録。

前提

  • Docker
    • docker-compose
  • デプロイできるAWS ECSクラスタが存在する
  • AWS ECR
  • github actions
  • Terraform

dockerの作成

ローカルで確認できるように&pushするときのタグを保存できるのでdocker-compose.ymlでbuildするようにする。

環境変数で基本必要なものを .env.docker に作っておいてsecretな情報は引き数で入れる方針で。

docker-compose.yml
version: '3.7'

volumes:
  sock:
    name: sock

services:
  nginx:
    image: 012345678901.dkr.ecr.us-east-1.amazonaws.com/app-nginx
    container_name: app-nginx
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    volumes:
      - sock:/sock
    depends_on:
      - php
    ports:
      - "8080:80"

  php:
    image: 012345678901.dkr.ecr.us-east-1.amazonaws.com/app-php
    container_name: app-php
    build:
      context: .
      args:
        SECRET_TOKEN: "$SECRET_TOKEN"
    volumes:
      - sock:/sock

php-fpm

FROM php:7.4-fpm-alpine

ENV COMPOSER_ALLOW_SUPERUSER 1
ARG SECRET_TOKEN

COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apk add --no-cache zip unzip

WORKDIR /code

COPY --chown=www-data:www-data . .
RUN composer install
COPY .env.docker .env
COPY docker/php-fpm.d /usr/local/etc/php-fpm.d
RUN echo SECRET_TOKEN="$SECRET_TOKEN" >> .env

RUN php artisan key:generate
RUN chown www-data /usr/local/var/log/

WORKDIR /code/public

php-fpmのコンテナだとzz-docker.confにlistenが入っているので上書きしないとsocketが書き込まれないので上書き

docker/php-fpm.d/zz-docker.conf
[global]
daemonize = no

[www]
listen = /sock/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0666

他php-fpmで変えたい項目があるなら、 www.conf も上書きすればよい

nginx

public/ にあるファイルは必要なので配置

FROM nginx:stable-alpine

ADD docker/nginx/conf.d/ /etc/nginx/conf.d
ADD public/ /code/public

EXPOSE 80

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

/sock/php-fpm.sock でボリューム共有するのでそこをfastcgi_passに指定

docker/nginx/conf.d/default.conf
server {
  listen 80 default_server;
  server_name _;
  charset utf-8;
  client_max_body_size 75M;

  gzip            on;
  gzip_types      text/plain application/xml text/css application/javascript;
  gzip_min_length 1000;


  index index.php index.html;

  root /code/public;

  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/sock/php-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
  }
}

下記実行して 127.0.0.1:8080 がひらけたら成功

docker-compose build
docker-compose up

TerraformでECR と ECS service用意

networkModeをawsvpcにしてdiscovery serviceで参照する設定いれてます。
security group、vpcとdiscovery serviceの設定は別のところでやっているとして省略。

github actions用のユーザterraformで作って ACCESS_KEYSECRET_ACCESS_KEY はtfstateにいれたくないからWebコンソールからとってます。

resource "aws_cloudwatch_log_group" "app" {
  name              = "/app"
  retention_in_days = 60

  tags = {
    Environment = "app"
  }
}

resource "aws_ecs_task_definition" "app" {
  family = "app"
  volume {
    name = "sock"
    docker_volume_configuration {
      scope  = "task"
      driver = "local"
    }
  }
  container_definitions = file("task-definition/app.json")
  network_mode          = "awsvpc"
}

resource "aws_ecs_service" "app" {
  name                               = "app"
  cluster                            = "cluster"
  task_definition                    = aws_ecs_task_definition.app.arn
  desired_count                      = 1
  deployment_minimum_healthy_percent = 0
  deployment_maximum_percent         = 100

  network_configuration {
    subnets = aws_subnet.vpc.*.id
    security_groups = [
      aws_security_group.output.id
      aws_security_group.app.id
    ]
  }

  service_registries {
    registry_arn = aws_service_discovery_service.app.arn
  }

  lifecycle {
    ignore_changes = [desired_count]
  }
}

resource "aws_service_discovery_service" "app" {
  name = "app"

  dns_config {
    namespace_id = aws_service_discovery_public_dns_namespace.main.id

    dns_records {
      ttl  = 10
      type = "A"
    }

    routing_policy = "MULTIVALUE"
  }

  health_check_custom_config {
    failure_threshold = 1
  }
}

# ECR
resource "aws_ecr_repository" "app-nginx" {
  name = "app-nginx"
}

resource "aws_ecr_repository" "app-php" {
  name = "app-php"
}

resource "aws_ecr_lifecycle_policy" "app-nginx" {
  repository = aws_ecr_repository.app-nginx.name
  policy     = file("ecr/lifecycle-policy.json")
}

resource "aws_ecr_lifecycle_policy" "app-php" {
  repository = aws_ecr_repository.app-php.name
  policy     = file("ecr/lifecycle-policy.json")
}


resource "aws_iam_user" "github-actions-app" {
  name = "github-actions-app"
  path = "/"
}


resource "aws_iam_user_policy" "github-actions-app" {
  name = "github-actions-app"
  user = "${aws_iam_user.github-actions-app.name}"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ecs:UpdateService",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:CompleteLayerUpload",
                "ecr:DescribeRepositories",
                "ecr:UploadLayerPart",
                "ecr:InitiateLayerUpload",
                "ecr:BatchCheckLayerAvailability",
                "ecr:PutImage"
            ],
            "Resource": [
                "arn:aws:ecs:us-east-1:012345678901:service/cluster/app",
                "arn:aws:ecr:us-east-1:012345678901:repository/app-*"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "ecr:GetAuthorizationToken",
            "Resource": "*"
        }
    ]
}
EOF
}

volumeをmountしてsocketファイルをコンテナ間で共有

task-definition/app.tf
[
    {
        "mountPoints": [
            {
              "sourceVolume": "sock",
              "containerPath": "/sock"
            }
        ],
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "/app",
                "awslogs-region": "us-east-1",
                "awslogs-stream-prefix": "nginx"
            }
        },
        "cpu": 32,
        "memoryReservation": 32,
        "image": "012345678901.dkr.ecr.us-east-1.amazonaws.com/app-nginx:latest",
        "portMappings": [
            {
                "hostPort": 80,
                "containerPort": 80,
                "protocol": "tcp"
            }
        ],
        "name": "nginx"
    },
    {
        "mountPoints": [
            {
              "sourceVolume": "sock",
              "containerPath": "/sock"
            }
        ],
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "/app",
                "awslogs-region": "us-east-1",
                "awslogs-stream-prefix": "php"
            }
        },
        "cpu": 64,
        "memoryReservation": 64,
        "image": "012345678901.dkr.ecr.us-east-1.amazonaws.com/app-php:latest",
        "name": "php"
    }
]

削除は単純なポリシーで

ecr/lifecycle-policy.json
{
    "rules": [
        {
            "rulePriority": 1,
            "description": "Expire images older than 7 count",
            "selection": {
                "tagStatus": "untagged",
                "countType": "sinceImagePushed",
                "countUnit": "days",
                "countNumber": 7
            },
            "action": {
                "type": "expire"
            }
        }
    ]
}

imageがない状態で terraform apply するとECS Serviceが起動でエラーでますが気にしない。
するなら、ECRのところだけ terraform apply して docker-compose push してからやるとECS Serviceも起動する。

github actions

githubのsecretsに AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY SECRET_TOKEN を作ってます。

自動でデプロイされてくないので tagに v* とつけたらデプロイされるようにしてます。

github/workflows/deploy.yml
name: Deploy

on:
  push:
    tags:
      - v*

jobs:
  deploy_production:
    name: deploy production
    runs-on: ubuntu-latest
    steps:
    - name: AWS ECR login
      run: $(aws ecr get-login --no-include-email)
      env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: us-east-1
    - uses: actions/checkout@master
    - name: Get Composer Cache Directory
      id: composer-cache
      run: |
        echo "::set-output name=dir::$(composer config cache-files-dir)"
    - uses: actions/cache@v1
      with:
        path: ${{ steps.composer-cache.outputs.dir }}
        key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
        restore-keys: |
          ${{ runner.os }}-composer-
    - name: build base
      run: docker-compose build
      env:
        SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
    - name: push image
      run: docker-compose push
    - name: force deployment ecr
      run: aws ecs update-service --cluster cluster --service app --force-new-deployment
      env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: us-east-1

これでgithubでreleaseからタグ打てばデプロイされて便利です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Laravel]RegisterController.phpさん、抽象化ダイエット大作戦!

Diverse Advent Calendar 2019 14日目の記事です。

こんにちは! @gatapon です!
エンジニア歴1年未満ですが無事生きてます。元気にしてます!
業務では使っているわけではないのですが、Laravelについて書いてみようと思います。

概要

少し設計に興味を持った私がRegisterController.phpをLaravelの機能を利用したり、レイヤー化させてみました。

やったこと

  • RegisterController.phpの処理の分離
  • Service層の導入
  • Repository層の導入

はじめに

 「どうしてこんな記事を書くのか」という経緯をポエムっぽく書きます。

 4月からエンジニアとして実務を経験してきましたが、入社前の私は個人で小さなポートフォリオを作った程度でした。実際に仕事で触れるプロダクトはポートフォリオと比べ物にならないくらい大きく、ちょっとした機能の修正であっても「どこに記述するべきか」、「どう記述するべきか」という事に悩む事が多かったです。

エンジニアになって3,4ヶ月程経ち、少しずつですが経験を重ね「ここはこう書いたほうが良いよな」みたいな勘が、なんとなくではありますが自分の中で形成されていきました。しかしそれはあくまで勘でしかなく、「なんとなく正しい」と思ってるだけであり、客観的に正しいかどうか自分には判断できませんでした。自分の中で言語化できておらず他人に伝えることも、なかなか大変でした。

 そういった事が原因か判りませんが、抽象的なものを読みたいという欲求に駆られていろいろ読み物を漁りました。コードを『どこに記述するべきか』『どう記述するべきか』について読み漁っていくうちに「設計」を勉強することがが近道ではないかなと感じ、少しずつ興味を持つようになりました。

以前、Laravelを使ってポートフォリオを作っていたのもあり、たまたま持っていた本もLaravelのクリーンアーキテクチャについて詳細に記述されていたのでLaravelを使って勉強してみることにしました。

その中でもLaravelのRegisterController.phpの書かれ方が気になったので、いろいろいじってみました。
それが割と面白かったので、簡単ではありますが内容をお伝えしたいと思います。

以下、出てくるソースは実装をフェイズごとに区切っていますが、勉強時にディレクトリ構造やファイル名、テーブル名など命名にも試行錯誤重ね、度々変更していました。記事中のファイル名などに不整合が起こっているかもしれません。統一するよう見直しましたが、漏れがあったらご指摘下さい。

RegisterController.phpを手直し

まずはそのままのRegisterController.phpを見てみます。

app/Http/Controllers/Auth/RegisterController.php

app/Http/Controllers/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------     
    | Register Controller
    |-------------------------------------------------------------------------- 
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name'  => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        return User::create([
            'name'  => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

ユーザーの新規登録の処理がコントローラー内にまとめられています。これは各プロジェクトごとに拡張しやすいように、良い感じにしてくれているのでしょう。RegisterController.phpにはvalidation, ユーザーの作成する処理はありますが、実体となるregisterメソッドはここにはありません。

route/web.php
Route::post('auth/register', 'Auth\RegisterController@register');

しかし、それでもRouteで呼ぶことができています。
それはregisterメソッドを持つRegistersUsersトレイトを継承しているからです。

framework/src/illuminate/Foundation/Auth/RegistersUsers.php

これらソースをもとに下準備を行い記述します。
一部修正というか、別のディレクトリにてファイルを作り直しています。

app/Http/Controllers/Api/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Profile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

    use RegistersUsers;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * register
     *
     * @param  Request
     * @return JsonResponse
     */
    public function register(Request $request)
    {
        $validate = $this->validator($request->all());
        if ($validate->fails()) {
            return new JsonResponse($validate->errors());
        }
        event(new Registered($user = $this->create($request->all())));
        if (empty($user)) {
            return new JsonResponse('Error');
        }
        return new JsonResponse($user);
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name'     => ['required', 'string', 'max:255', 'unique:users'],
            'email'    => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    /**
     * Transaction for create a new user and profile, after a valid registration.
     *
     * @param  array $data
     * @return \App\Models\User
     */
    protected function create(array $data)
    {

        DB::beginTransaction();
        try {
            $user = User::create([
                'name'     => $data['name'],
                'email'    => $data['email'],
                'password' => Hash::make($data['password']),
            ]);
            Profile::create([
                'user_id'  => $user->id,
                'country'  => '',
                'question' => '',
                'answer'   => '',
            ]);

            DB::commit();
            return $user;

        } catch(\PDOException $e) {
            DB::rollBack();
            Log::Debug('Transaction Error: '. print_r($e, true));
        }
    }
}

app/Http/Controllers/Api/RegisterController.phpを作成し、以下を実施しました。

  • RegistersUsersregisterメソッドをもってくる。
  • 手軽に検証できるようにregisterメソッドはJsonを返すように修正
  • createメソッドにユーザー新規作成と同時にProfileも作成するトランザクションを追加

いい感じに肥えて来ましたね!
その他、Profileモデルの実装やテーブル等の準備はありますが端折ります。

作成したRegisterController.phpのダイエット

準備ができました。
早速以下2つを行いRegisterController.phpをスッキリさせます。

  • LaravelのFormRequest機能を利用
  • Service層を作成して処理層を分離

これらを行うことで、コントローラーは「リクエストを受け取り、要求されたデータを返す」だけのシンプルな形になります。

app/Http/Controllers/Api/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\UserRegistPost;
use App\Services\UserRegisterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

    use RegistersUsers;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(UserRegisterService $service)
    {
        $this->middleware('guest');
        $this->service = $service;
    }

    /**
     * register
     * @param  UserRegistPost
     * @return JsonResponse
     */
    public function register(UserRegistPost $request)
    {
        # userRegisterTransactionについては後述
        event(new Registered($user = $this->service->userRegisterTransaction($request->all())));
        if (!$user) {
            return new JsonResponse('Error');
        }
        return new JsonResponse($user);
    }
}

FormRequest

LaravelにあるFormRequestはとても便利です。バリデーションのロジックをコントローラーから分離できます。
また、コントローラーのメソッドに依存注入することでコントローラーがリクエストを受け取った時点でバリデーションが行われます。
registerメソッド内にバリデーションエラー処理も記述しなくて済み、コントローラーがスッキリしました。

app/Http/Requests/UserResistPost.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Traits\ApiFormRequestTrait;

class UserRegistPost extends FormRequest
{
    use ApiFormRequestTrait;

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name'  => ['required', 'string', 'max:255', 'unique:users'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ];
    }
}

今回のAPIのFormRequestのValidationエラーの実装は、以下リンクを参考にさせてもらいました。これ以外の方法もあるようです。脇道にそれるので、ここでは記述しません。

【Laravel5】FormRequestのバリデーション結果をJSON APIで返す

Service層の導入

MVCの下に作成されたフレームワークにはFatモデル、Fatコントローラー問題があります。
Service層の導入はその問題を緩和することが出来ます。
モデルやコントローラーからビジネスロジックを分離させることができるので、本来の責務だけ任せることが出来ます。

app/Services/UserRegisterService.php
<?php

namespace App\Services;

use App\Models\User;
use App\Models\Profile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;

class UserRegisterService
{
    /**
     * Transaction for create a new user and profile, after a valid registration.
     *
     * @param  array $data
     * @return User
     */
    public function userRegisterTransaction(array $data)
    {
        DB::beginTransaction();
        try {
            $user = User::create([
                'name'     => $data['name'],
                'email'    => $data['email'],
                'password' => Hash::make($data['password']),
            ]);
            Profile::create([
                'user_id'  => $user->id,
                'country'  => '',
                'question' => '',
                'answer'   => '',
            ]);

            DB::commit();
            return $user;

        } catch(\PDOException $e) {
            DB::rollBack();
            Log::Debug('Transaction Error: '. print_r($e, true));
        }
    }
}

メソッド名をuserRegisterTransactionに変更していますが、ただ単にapp/Services/UserRegisterService.phpにお引越ししてきただけです。
これでRegisterController.phpのダイエットは完了です。

UserRegisterService.phpさんにもダイエットしてもらおう

Repository層の導入

Service層はビジネスロジックを分離できるメリットがあるとお話しましたが、移植してきたものの内容を見てみるとデータの操作ぐらいしか行っていません。と言うか、ビジネスロジックに集中したいService層でデータアクセスを直接行っています。この書き方はもう少し変えていきたいと思います。
ビジネスロジックからデータ操作を切り離すことで、テスト容易性や、保守性、拡張性が保証されます。そこでドメイン駆動開発にも紹介されているRepository層を導入してみようと思います。


注意
実際問題、めったにデータソースが変わることがないサーバーにRepository層を装する必要は、あまりないかもしれません。レイヤー化、抽象化を目的とした設計なら他の方法があるかもしれませんね。
参考:“Repositoryによる抽象化の理想と現実/Ideal and reality of abstraction by Repository - Speaker Deck”


抽象クラス(RepositoryInterface)と具象クラス(Repository)の実装

私は今のところ、Repository層の最大の目的はデータの永続性だと理解しています。データソースが変更された時(MySQL→NoSQLなど)、移行の対応を極力抑えることが出来ます。各RepositoryInterfaceを継承した具象クラスを実装するだけなので、ビジネスロジックに手を加える必要がなくなります。

User, Profileそれぞれの今回使うメソッドのみの実装を行っていきます。

RepositoryInterfaceの実装

app/Repositories/Interfaces/UserRepositoryInterface
<?php

namespace App\Repositories\Interfaces;

interface UserRepositoryInterface
{
    public function register(array $data)
}
app/Repository/Interfaces/ProfileRepositoryInterface.php
<?php

namespace App\Repositories\Interfaces;

interface ProfileRepositoryInterface
{
    public function create(int $user_id);
}

インターフェイスはあくまで骨組みだけになります。各データソースごとに、このInterfaceを継承した具象クラスを作成することで変更の対応が容易になります。(後に述べるInterfaceとのバインディングを変更する必要があります)
今回のデータ操作はLaravelのORM、Eloquentを利用していきます。
クエリビルダ、SQLを使うことも出来ます。

Repositoryの実装

app/Repositories/UserRepository.php
<?php

namespace App\Repositories;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use App\Repositories\Interfaces\UserRepositoryInterface;

class UserRepository implements UserRepositoryInterface
{
    protected $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * Create User
     * 
     * @param  $data
     * @return \App\Models\User
     */
    public function register(array $data)
    {
        return $this->user->create([
            'name'     => $data['name'],
            'email'    => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}
app/Repository/Interfaces/ProfileRepository.php
<?php

namespace App\Repositories;

use App\Models\Profile;
use App\Repositories\Interfaces\ProfileRepositoryInterface;

class ProfileRepository implements ProfileRepositoryInterface
{
    protected $profile;

    public function __construct(Profile $profile)
    {
        $this->profile = $profile;
    }

    /**
     * Create Profile
     *
     * @param  int $user_id
     * @return void
     */
    public function create(int $user_id)
    {
        $this->profile->create([
            'user_id'  => $user_id,
            'country'  => '',
            'question' => '',
            'answer'   => '',
        ]);
    }
}

Repository層を実装したUserRegisterService.phpは以下になります。
userRegisterTransactionメソッドに依存注入されているのは具象クラスでなく抽象クラスであるInterfaceであることに注意して下さい。

app/Services/UserRegisterService.php
<?php

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Repositories\Interfaces\ProfileRepositoryInterface;

class UserRegisterService
{
    protected $user;
    protected $profile;

    public function __construct(UserRepositoryInterface $user, ProfileRepositoryInterface $profile)
    {
        $this->user    = $user;
        $this->profile = $profile;
    }

    /**
     * Transaction for create a new user and profile, after a valid registration.
     *
     * @param  array $data
     * @return \App\Models\User
     */
    public function userRegisterTransaction(array $data)
    {
        DB::beginTransaction();
        try {

            $newUser = $this->user->register($data);
            $this->profile->create($newUser->id);

            DB::commit();
            return $newUser;

        } catch(\PDOException $e) {
            DB::rollBack();
            Log::Debug('Transaction Error: '. print_r($e, true));
        }
    }
}

RepositoryとInterfaceをバインディング

新しくRepository用のServiceProviderを作成するので、config/app.phpに追加します。

config/app.php
〜〜
    'providers' => [

     /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\RepositoryServiceProvider::class, # 追加

    ],

RepositoryServiceProvider.phpを作成し、抽象クラスと具象クラスをバインディングすれば実装完了です。データソースが違う具象クラスを利用する場合、こちらでInterfaceとバインディングされた具象クラスを変更します。

app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            \App\Repositories\Interfaces\ProfileRepositoryInterface::class,
            \App\Repositories\ProfileRepository::class
        );

        $this->app->bind(
            \App\Repositories\Interfaces\UserRepositoryInterface::class,
            \App\Repositories\UserRepository::class
        );
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

実装してみて

一年ぐらい前、エンジニアになるためにLaravelを勉強し始めたのですが、その時とコードの見た目が全く違うなあという印象です。コンストラクタにてDIを利用しクラスをセットしているので、依存クラスがひと目で分かりますね。実装時はディレクトリ構成とクラス名を考えるのに苦戦しました。実際にクラスを増やしていって見ていかないと、よりベターなディレクトリ構成やクラス名を付けれそうにないですね...
しかし機能やレイヤーをきっちり分けると、どこに何を書くべきかが判断しやすくなりました。面白かったので引き続き勉強していきたいと思います。


参考にした記事
- Laravelで実践クリーンアーキテクチャ
- Laravelでクリーンアーキテクチャ

参考にした書籍
- PHPフレームワーク Laravel Webアプリケーション

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaraDockを使って10分でLaravel+Nginx+MySql+Redisのローカル環境を構築してみる。

はじめに

こちらは TECOTEC Advent Calendar 2019 の13日目の記事です。
折り返しとなります。

Qiitaの記事や他所のブログでも散々書かれていることかと思いますが、復習がてらLaraDockを使ってLaravelのローカル環境を構築してみようと思います。
目指せ10分でローカル環境構築!

※当方macで作業しているため、macでの手順になります。Windowsの方は適時Windows版に読み換えてお試しください。

やること

  • Dockerのインストール
  • LaraDockのインストール
  • Laravelプロジェクトの作成
  • LaraDockの設定変更
  • Laravelの設定変更

以上となります。

ではさっそくやってみましょう。

実演

Dockerのインストール

以下のサイトよりDocker for Macをダウンロードします。

https://docs.docker.com/docker-for-mac/install/

ダウンロードにはdocker hubへの登録が必要なので案内に従って登録してください。
Docker.dmgをダウンロードできたら起動してインストールします。
インストール後はdocker.appを起動しておいてください。

LaraDockのインストール

GitHubからLaraDockのファイルを取得します。
ターミナルを起動して作業ディレクトリへ移動してください。

ターミナルで作業
$ cd ~/work

cloneします。

ターミナルで作業
$ git clone https://github.com/LaraDock/laradock.git

取得できました。laradockディレクトリが作成されています。
ワークスペースのコンテナを起動するために.envファイルを作ります。

ターミナルで作業
$ cd laradock
$ cp env-example .env

Laravelプロジェクトの作成

※開発が進んでいる場合はこの作業はスキップしてください。
※チーム等で管理しているGitからソースを取得してください。

ワークスペースのコンテナを起動してログインします。

ターミナルで作業
$ pwd
~/work/laradock
$ docker-compose up -d workspace
~~~省略
Creating laradock_workspace_1        ... done

$ docker-compose exec --user=laradock workspace bash
laradock@0c7610d35e08:/var/www$ 

ログイン完了です。
Laravelプロジェクトを作成します。

workspace内で作業
$ composer create-project laravel/laravel server

serverプロジェクトが作成されました。

LaraDockの設定変更

ワークスペースからログアウトします。

workspace内で作業
$ exit

vim.envファイルを開きます。(エディタならなんでもいいです)

ターミナルで作業
$ pwd
~/work/laradock
$ vim .env

プロジェクトのパスを変更します。
元の記述をコメントアウトして追記するか、直接編集してください。

.env
# APP_CODE_PATH_HOST=../
APP_CODE_PATH_HOST=../server/

MySqlのデータを永続化させないために作業ディレクトリへ変更します。
※プロジェクトごとにデータを管理する想定です。共通で問題ない場合はこの作業はスキップしてください。。

.env
# DATA_PATH_HOST=~/.laradock/data
DATA_PATH_HOST=.laradock/data/

ポートを変更します。
※こちらは任意です。必要なければこの作業はスキップしてください。

.env
# NGINX_HOST_HTTP_PORT=80
NGINX_HOST_HTTP_PORT=8880

MySqlのバージョンと接続情報を変更します。
最新の8.xは色々あれなので5.7を指定します。
※最新で問題ない場合はこの作業はスキップしてください。

.env
# MYSQL_VERSION=latest
# MYSQL_DATABASE=default
# MYSQL_USER=default
MYSQL_VERSION=5.7
MYSQL_DATABASE=laradock
MYSQL_USER=laradock

MySqlとRedisのホスト名を設定しておきます。
ファイル内の一番下に追記してください。

.env
DB_HOST=mysql
REDIS_HOST=redis

設定を反映させるためコンテナを再起動します。
一緒にNginx・MySql・Redisを起動します。

ターミナルで作業
$ pwd
~/work/laradock
$ docker-compose stop
$ docker-compose up -d workspace nginx mysql redis

Laravelの設定変更

ワークスペースのコンテナにログインして作業します。

ターミナルで作業
$ pwd
~/work/laradock
$ docker-compose exec --user=laradock workspace bash
laradock@0c7610d35e08:/var/www$ 

.envを編集します。

workspace内で作業
$ vim .env

DBとRedis接続設定を書き換えます。

.env
DB_HOST=mysql
DB_DATABASE=laradock
DB_USERNAME=laradock
DB_PASSWORD=secret
~~
REDIS_HOST=redis

パーミッションを変更します。

workspace内で作業
$ chmod -R a+w storage
$ chmod -R a+w bootstrap/cache

依存ライブラリをインストールします。

workspace内で作業
$ composer install

接続確認!

http://localhost:8880/ にアクセスします。

スクリーンショット 2019-12-13 11.41.49.png

Laravelのトップページが表示されれば完了です。
非常に簡単にLaravelの環境が構築できました。
ホストやDBの情報などは適時自分の使いやすいように変更してください。
NginxやMySqlの細かい設定はまた別の機会にかければ書きたいと思いいます。
というかそのままでローカルで使う分には全く問題ないのであまりいじったことがないです。

おまけ

Laravelには便利な機能が沢山あります。

認証機能の実装

以下のコマンドを実行するだけで登録・ログイン・パスワードリセットなどが使用できるようになります。

workspace内で作業(ver5.xの場合)
$ php artisan make:auth
workspace内で作業(ver6.xの場合)
$ composer require laravel/ui
$ php artisan ui vue --auth

Laravelのバージョンで若干コマンドが異なるので注意が必要です。

表示を整えるためにnpm installとnpmを実行してcssとjsをコンパイルします。

workspace内で作業
$ npm install && npm run dev

マイグレーションを実行します。

workspace内で作業
$ php artisan migrate

認証機能の完成です。
スクリーンショット 2019-12-13 11.44.16.png
スクリーンショット 2019-12-13 11.46.01.png
スクリーンショット 2019-12-13 11.46.09.png

管理画面の実装

ついでに管理画面も作ってみます。

workspace内で作業
$ composer require encore/laravel-admin

以下のコマンドを実行します。

workspace内で作業
$ php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider"
$ php artisan admin:install

http://localhost:8880/admin/ にアクセスします。
初期のIDパスワードは、ID:adminパスワード:adminとなっています。

スクリーンショット 2019-12-13 11.49.58.png
スクリーンショット 2019-12-13 11.51.04.png

そのままで使えることは少ないですが、上記で認証機能と管理画面が作れてしまいます。

起動シェルを作ってみる

PC起動後など起動コマンドが長かったりするのでシェルを作っておくと捗ります。

起動

up.sh
#!/bin/bash
cd ./laradock; docker-compose up -d workspace nginx mysql redis

ワークスペースへログイン

exec_workspace.sh
#!/bin/bash
cd ./laradock; docker-compose exec --user=laradock workspace bash

停止

down.sh
#!/bin/bash
cd ./laradock; docker-compose down

まとめ

LaraDockはローカル環境構築が非常に簡単です。
PHPでのメインフレームワークはしばらくLaravelが多くなりそうなので、LaraDockは扱えるようにしておくと色々便利そうです。
簡単なwebページ作るならあっという間です。

今後も色々試して使いこなしていきたいと思います。

よいエンジニアライフを!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelとSpringBootでDIコンテナを利用してみる

はじめに

これはユアマイスターAdventCalendar2019の13日目の記事です。
(社会人になってから学んだことをアウトプットする記事になります。)

今回の経緯

社会人になってからDI(依存性の注入)という概念を知りました。
WEBサービスの開発を行う際に、フレームワークを利用する場面は多々ありましたが、主に利用していたCake PHPではDIという概念は出ていなかったと記憶しています。
(記憶違いだったらすみません。)

DIは、インスタンスをnewで作成して利用するのではなく、DIコンテナやサービスコンテナ呼ばれるもの(SpringBootではDIコンテナ、Laravelではサービスコンテナと呼ばれます)を利用して、あらかじめ登録されたインスタンスを利用します。

今回は、業務で利用しているSpringBootでのDIコンテナの利用、最近独学で学んでいるLaravelでのサービスコンテナを利用してみるというテーマで記事を書いてみたいと思います。

SpringBootの場合

コンストラクターインジェクションを利用する

ユアマイスターアドベントカレンダー2019 の7日目の記事で書いたコードを用いて書いていきます。
SpringBootで動的にDBを切り替えてみる
https://github.com/Masaki-Ogawa/datasourceDemo

  1. PersonRepository.java
PersonRepository.java
package com.example.dataSourceDemo.domain.repositories;

import com.example.dataSourceDemo.domain.models.Person;
import org.springframework.data.jpa.repository.JpaRepository;

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {

}

JpaRepositoryを継承したRepositoryクラス

  1. PersonServiceImpl.java
PersonServiceImpl.java
package com.example.dataSourceDemo.domain.services;

import com.example.dataSourceDemo.annotations.DataSource;
import com.example.dataSourceDemo.annotations.DataSource.DataSourceType;
import com.example.dataSourceDemo.domain.models.Person;
import com.example.dataSourceDemo.domain.repositories.PersonRepository;
import java.util.List;
import org.springframework.stereotype.Service;

@Service
public class PersonServiceImpl implements PersonService {

  private final PersonRepository personRepository;

  public PersonServiceImpl(
      PersonRepository personRepository) {
    this.personRepository = personRepository;
  }

  /**
   * stgのDBからPersonテーブルのレコードを取得するメソッド
   * @return Personテーブルのレコード
   */
  @Override
  public List<Person> findAllPersonInStg() {
    return personRepository.findAll();
  }

  /**
   * stgのDBからPersonテーブルのレコードを取得するメソッド
   * @return Personテーブルのレコード
   */
  @DataSource(value = DataSourceType.PROD)
  @Override
  public List<Person> findAllPersonInProd() {
    return personRepository.findAll();
  }
}

ここでDIを行っています。

具体的には、

private final PersonRepository personRepository;

  public PersonServiceImpl(
      PersonRepository personRepository) {
    this.personRepository = personRepository;
  }

の部分でコンストラクターインジェクションによるDIを行っています。
@Service@Repositoryというアノテーションを利用することにより、DIコンテナに登録されます。
利用するには上記のように、コンストラクターインジェクション等を利用して、インスタンスを作成します。

参考
Spring Framework 要点まとめ ~ DIについて

LaravelでのDI

サービスプロバイダーを利用する

作成したサービスをサービスコンテナ登録するためのサービスプロバイダーを作成します。
今回はDemoServiceというサービスクラスを作成しました。

AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind('App\Services\DemoService');
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

上記のようにサービスを登録します。

DemoService.php
class DemoService {
    public function show() {
        echo "何かを表示します";
    }
}

サービスクラスは今回は適当ですが、何か文字を出力するメソッドのみ実装します。

DemoController.php
class MessageController extends Controller
{
    protected $demoService;

    public function __construct(DemoService $demoService)
    {
        $this->demoService = $demoService;
    }

    public function index(Request $request) {

        return $this->demoService->show();
    }
}

このようにこちらもコンストラクターインジェクションを利用して、こちらもDIを行います。

終わりに

個人的には、@Service@Controller等のアノテーションで、DIコンテナに登録できるSpringBootの方が利用しやすいと感じています。
Java、SpringBootを利用してやはり、アノテーションの強力さに気づかされる場面が多々あります。

また、7日目のアドベントカレンダーで書きましたが、それぞれのフレームワークに良さや悪さがあり、多数のフレームワークに触るという経験は、今後何かものを作るときに、「どんなものを採用すれば、そのプロダクトにとって一番良いのか?」という判断材料になると思います。

今後も、業務、業務外を含めて触れていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カルネージハートとは関係ないLaravelのnow()の小ネタ

カルネージハート Advent Calendar 2019 11日目の記事です。

今回はカルネージハートとは全く関係ないLaravelのnow()の小ネタの話です。

はじめに

公式ドキュメントのLaravel 6.x ヘルパ見てますか?

現在時刻が欲しいだけならuse Carbon\Carbon;は不要

そのまんまです。

Screenshot from Gyazo

now();

よくuse Carbon\Carbon;宣言したり、Laravelでdate関数使ってごにょごにょしているの見て保守するの辛いんだなこれが

で現在時刻取れます。

最後に

事実上の最新作EXAが2010年に発売以降続編の情報が皆無ですが、一部の熱狂的ファンは大会を開催してゲームを続けています。ゲームを盛り上げることで続編も出るかもしれません。カルネジスト、ネジらーの皆様のご協力をお願いします!

カルネージハートファンのプログラミング知識を共有しましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】初心者から実務をこなしていくまでの6ヶ月にやったこと

CODEBASE2期生、新卒1年目の @avocadoneko です。
仕事では主に Laravel / Vue.js を使って、開発をしています。

この記事について

どんな人に読んでほしいか

  • プログラミングを学び始めたばかりの人
  • これから PHP, Laravel を学びたいと思っている人
  • Laravel で開発している会社に入社する予定の人

書いてあること

  • 初心者が PHP と Laravel をどうやって勉強してきたか
  • 仕事で学んだ Laravel のこと

書いてないこと

  • PHP、Laravel 以外のこと(Git とか Web のこととか他の言語とか)

注意

  • いくつか紹介している記事や動画では、Laravel のバージョンが古いことがあります。

やったこと

まずはPHPを触ってみる

Progate

[logo_1.jpg

ゲーム感覚でプログラミングを学べる教材。
有料版は月額980円。
超初心者が PHP はどんな言語なのか知るのの導入に役立つと思う。実際、プログラミングが何もわからない状態でもサクサクと進めることができた。
但し、Progate だけやっていても仕方がないので、基礎を学んだ後はやらなくてもいいと思う。

ドットインストール

[logo_2.jpg

月額1,080円(無料でも多くの動画を視聴できる)で手軽に学べる動画教材。
ローカル環境構築の方法も動画になっているので、動画を見るだけではなく、自分のPCで再現して学ぶのがおすすめ。
手を動かして学ぶのが好きな人には向いていると思う。
Laravel の動画に入る前に、PHPで小さいアプリケーションを作る動画をやるのが良さそう。

以下、良いなと思った動画↓

書籍

詳細! PHP 7+MySQL 入門ノート

book_1.jpg

この本は最初から読んでいって、知らなかったことだけ自分のPCで実行してみると良い。
また、 一番読んでほしいところは Chapter7 の「オブジェクト指向プログラミング」の章。
クラスの定義方法、継承、コンストラクタなど知っておくべき基本的なことについてについて、実際のコードと共に説明されている。

PHPフレームワーク Laravel入門

[book_2.jpg

Laravel 入門書として一番おすすめの本がこちら。
レビューにもある通り、かなり回りくどく書かれているからこそ、入門書として人気が高い。
この本の使い方は、とりあえず写経。
書いてあることは全部必要な知識だから、隅から隅まで読むべき。
写経は GitHub でリポジトリを作って動く単位でコミットしていき、コミットメッセージに気づいたことを書くといいと思う。
写経のやり方については下記の記事にわかりやすく書いてある。
技術書の写経を始めたのでやり方を書いておく

完全に理解しようとして行き詰まるより、とにかく読み切ることを目標にして、70~80%くらいの理解で最後のページまで写経するといいかもしれない。
また、丸々一冊読み切った本は、後に振り返るのにも便利。

記事

【PHP超入門】クラス~例外処理~PDOの基礎

$this とか スコープ定義演算子とか、とにかく PHP の基礎が全くわかってなかった頃にお世話になった記事。
初心者がつまずきやすいところがピンポイントで書かれていて、何度も助けられた。

チュートリアル

Laravel 5.5 入門として「基本のタスクリスト」を作成する

検索したら大量に出てくるチュートリアルの中でも、これは特に分かりやすかったなという印象。コントローラを使わずに web.php に処理を書いてタスクリストを実装しているので、使用するファイルが少なく、混乱せずにできた。
(実際開発するときはありえないけど、初心者には向いているチュートリアルだと思う)
さらなるステップとして、ここに機能を追加していくのもいい。私の場合は編集機能を追加した。

Laravelで飲食店検索LINE Botを作ろう!

このチュートリアルは私はやったことがないが、もっと前からあったらやってみたかった。
有料だが、Docker 開発環境の準備から丁寧に説明しているチュートリアルは少ない上、LINEbot を作るまでできたらそれなりに達成感がありそう。

Laravelでオリジナルのアプリを作る

DB 設計から機能、UI まで、一つのアプリを作ってみるのがおすすめ。
先に完成物の機能を具体的に決めてから実装したほうが良い。
自分が作りたいものを作るのが一番良い。
私の場合は下記のような機能を実装した。

  • 新規登録 / ログイン機能
  • イベントの作成 / 編集 / 削除
  • イベントごとに写真を追加 / 削除

テーブル数は3-4つくらいが丁度よかった。
余裕があったら、API叩いて、画面遷移なしでイベントや写真を作成・削除できるようにしてもいいかも。
自分でアプリを作ることで、自分がわからない部分が明確になるし、完成させたら自信になる。

業務に入ってから

仕事では独学と違って、0から何かを作ることは少ない。
初めて会社のプロダクトコードをみたときは、ファイルとコード量が多すぎてびっくりした。
業務ではチームで開発するし、人の入れ替わりがあるので保守性を意識する必要があるため、「動く」だけでなく「読める」コードを書く意識をしなければいけない。

書籍

PHPフレームワーク Laravel実践開発

book_4.jpg

前に紹介した書籍「PHPフレームワーク Laravel入門」(以下、青本)の中級者向けバージョン。
青本を読み終わった人向け。
同じ著者なので、青本が読みやすくて気に入ったら、こちらも読んでみると良い。
レビューで内容が薄いとあるが、その分気軽に読めるので全体を流し読みするのには最適。足りない部分はググって補うといいかと思う。
また、これもレビューにある通り、誤字脱字は多いのでたまに自分のPCで動かしてみても動かないことがあるので注意。

PHPフレームワーク Laravel Webアプリケーション開発

book_3.jpg

この本は、仕事でコードを書く上でのユースケースがたくさん書かれている。
どうやって実装したらいいか迷ってるときに、この本に手を伸ばすと、解決することがある。
全部読むというより、辞書代わりに手元に置いておくと良い。

記事

Laravelで始める依存性の注入(DI)

DIってなんだ??となったときに読んで役に立った記事

Laravelで実践クリーンアーキテクチャ

Laravel をクリーンアーキテクチャに当てはめるなら、どういう感じになるのか?そもそもクリーンアーキテクチャって???となったときに読んで役立った記事。

わからなくなったら

Laravel 日本語公式ドキュメント

実装中にわからないことがあってググった時、まずは公式ドキュメントを読むのがいい。
私は最初、書いてある日本語が難しくて読めなかったから、ある程度 Laravel に慣れてきたらでもいいと思う。
バージョンの違いや正確性を考慮すると、公式ドキュメントを読むのがいちばん(当たり前のことだけど)。

Laravel API

ドキュメントに書いてないメソッドなどもここで検索すれば出てくる。

最後に

いくつか宣伝をさせてください。

CODEBASE 沖縄 プログラミング教室

学生時代、エンジニアになるために通っていたスクールです。
3ヶ月間で、何もわからない状態からソフトウェアエンジニアとして新卒就職できるまで成長できました。

千株式会社

私が新卒で入社した会社です。
千株式会社 では幼稚園・保育園向けインターネット写真サービス「はいチーズ!」を提供しています。
新卒、中途共に絶賛採用中です!

週1回のリモートワーク、フレックス制度(コアタイムが12:00-15:00)など、柔軟な働き方ができる会社です。(朝の6時に出社すれば、なんと15時に退勤できてしまいます...!)
モダン(Laravel + Docker + CircleCI + AWS)で自由な環境で働いてみたい方におすすめの会社です。
もっと詳しく知りたい方は こちら

アドベントカレンダー 千 Advent Calendar 2019もやっているのでぜひ覗いて行ってください〜


この記事は CODEBASE okinawa Advent Calendar 2019 13日目の記事です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel(5.8)のFormファザードサンプル

概要

見た目がスッキリするのでLaravelの拡張としてよく使うFormFacade、ただ書き方を忘れる事があるのでメモとして残しておく。
公式はこちら

環境

  • laravel:5.8.*
  • laravelcollective/html: ^5.8

前提

  • old()はLaravelのヘルパーであり、直前のフォームに入力した値を取得する働きをする。
    • 第2引数は初期値
  • BootStrap4を使っているのでformで使うclassを指定している

  • セレクトボックス・ラジオボタン・チェックボックスで使う配列は下記のような形とする

$array = [
    1 => 'hoge',
    2 => 'fuga',
    3 => 'piyo',
];

フォームの開始と終了

// 開始
{{ Form::open(['route' => ['user.update', 'user' => $user->id], 'method' => 'put']) }}

// 終了
{{ Form::close() }}

テキスト

一番使う基本的な形
emailとかpasswordとかはほぼ同じ形なので省略

{{ Form::text('name', old('name', $user->name), ['class' => 'form-control']) }}

セレクトボックス

第2引数に配列、第3引数に初期値を入力すればよい

{{ Form::select('sample_id', $array , old('sample_id', $user->sample_id) , ['class' => 'form-control']) }}

ラジオボタン

第3引数についてはbooleanを設定する。
この場合は三項演算子の省略でtrueかfalseを返すようにしている。
注意点として配列のキーに0をもたせていると強制一致してしまう場合があるのでその場合は===を利用する。
ここで真偽値表を確認するとよい

@foreach($array as $key => $val)
  {{ Form::radio('sample_radio', $key, ($key == old('sample_radio', $user->sample_radio)), ['id' => 'radio'.$key]) }}
  {{ Form::label('radio'.$key, $val) }}
@endforeach

チェックボックス

第3引数についてはbooleanを設定する。
この場合はin_arrayの戻り値を利用している。($keyが各配列に存在するかチェックしている)
in_arrayの比較が不安だという方はin_arrayの第3引数にtrueと書きましょう。

@foreach($array as $key => $val)
  {{ Form::checkbox('sample_check[]', $key, in_array($key, old('sample_check', $user->sample_check)), ['id' => 'check'.$key]) }}
  {{ Form::label('check'.$key, $val) }}
@endforeach

テキストエリア

HTMLを出力したい場合があるのでサンプルでは「!!」でエスケープ処理を解除した例を書いておく

{!! Form::textarea('memo', old('memo', $user->memo), ['class' => 'form-control']) !!}

最後に

自分用のメモですが、誰かのためになれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む