作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 4章 後半資料 レスポンス ミドルウェア

以下の記事は2019年8月8日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読&勉強会 HTTPリクエストとレスポンスの輪読会資料の一部となります。
今回は 4章後半 4-3『レスポンス』, 4-4『ミドルウェア』部分の記事をアップします。

また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

また書籍のコードが記載されているGitHubレポジトリは以下となります。とても助かります!
GitHub - laravel-socym/chapter04-2: 4章 リクエスト・レスポンス

4-3 レスポンス

4-3-1 さまざまなレスポンス

Responseファサードの実態は Illuminate\Contracts\Routing\ResponseFactoryクラスである。
ファクトリークラスなので呼び出す生成メソッドで実際に生成されているResponseクラスは異なる。
アプリケーションからユーザーに返却するデータの種類でうまく使い分けを。

ここではデータタイプごとの返却方法を紹介する

文字列返却

シンプルなパターン、ダイレクトに下記例 hello world みたいに与えるか
Response メソッドの第三引数に 'content-type' => 'text/plain' を指定する

chapter04-2/PlainTextAction.php at master · laravel-socym/chapter04-2 · GitHub

上記サイトよりコード引用

<?php

declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

// 同一クラス名となりますので、このサンプルではファサードを別名としています
use Illuminate\Support\Facades\Response as LaravelResponse;
use function response;

class PlainTextAction
{
    public function __invoke(Request $request): Response
    {
       // 同一クラス名となりますので、このサンプルではファサードを別名としています
        $response = LaravelResponse::make('hello world');
        // ヘルパー関数を利用する場合
        $response = response('hello world');
        // content-typeを変更
        $response = response('hello world', Response::HTTP_OK, [
            'content-type' => 'text/plain'
        ]);
        return $response;
    }
}

テンプレート出力

標準のBladeテンプレート view を使う場合等、 View ファサードを使う。responseヘルパーメソッド等、まあ普通に勉強していると最初の当たり前として出る奴。

chapter04-2/BladeAction.php at master · laravel-socym/chapter04-2 · GitHub

<?php
$response = Response::view('view.file');

// 上記のメソッドと同じ結果が得られます
$response = view('view.file');

// ステータスコードを変更し、ビューを出力します。
$response = response(view('view.file'), Response::HTTP_ACCEPTED);

JSON出力

chapter04-2/JsonAction.php at master · laravel-socym/chapter04-2 · GitHub

<?php
$response = Response::make('hello world');

// ヘルパー関数を利用する場合
$response = response('hello world');

// content-typeを変更
$response = response('hello world', Response::HTTP_OK, [
    'content-type' => 'text/plain'
]);

任意のメディアタイプ

return response()->json(['message' => 'laravel'], Response::HTTP_OK, [
    'content-type' => 'application/vnd.laravel-api+json'
]);

また、jsonp を使いたい場合は 上記のメソッド名の json を jsonp に変えるだけ

ダウンロードレスポンス

ダウンローダー等DLをレスポンスさせたい場合は downloadメソッドを使う

chapter04-2/DownloadAction.php at master · laravel-socym/chapter04-2 · GitHub

<?php
$response = Response::download(storage_path('app/cover_min.png'));

// ヘルパー関数を利用する場合
$response = response()->download(storage_path('app/cover_min.png'));

// 第二引数、第三引数に任意の指定を行うパターン
$response = response()->download(storage_path('app/cover_min.png'), 'image.png', [
    'content-type' => 'image/png',
]);

return $response;

リダイレクトレスポンス

リダイレクト先を指定する奴

HTTPパラメーターを渡したい場合は withInputメソッド
リダイレクト→エラーメッセージ等の場合は with メソッドを使う

<?php
// どれも同じ結果なのでお好みで
$response = Response::redirectTo('/');
$response = response()->redirectTo('/');
$response = ridirect('/')

// リダイレクト時に動作を行う例
$response = ridirect('/')
  ->withInput($request->all())
  ->with('error', 'Validation error!!');

return $response

Server-Sent Events 実装

Server-Sent Events(SSE)はhtml5からの機能、サーバー側からのプッシュデータ通信を利用できるがWebSocketと異なり、双方向通信は出来ない。

chapter04-2/StreamAction.php at master · laravel-socym/chapter04-2 · GitHub

書籍だと以下の様に response()->stream( ... ) メソッドを使っているが、 現在のLaravelでは? response()->StreamedResponse( ... ) が使える様だ

LaravelでSSE(Server Sent Events)を利用してサーバから通知する | Minory

また、上記の記事でSSEに加えてクライアント側から非同期通信でステータス情報を送る事で、疑似的な双方向通信を行う事ができるそうです。

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;

class StreamAction
{
    /**
     * @return StreamedResponse
     */
    public function __invoke(): StreamedResponse
    {
        return response()->stream(function () {
            while (true) {
                echo 'data: ' . rand(1, 100) . "\n\n";
                ob_flush();
                flush();
                usleep(200000);
            }
        }, Response::HTTP_OK, [
            'content-type'      => 'text/event-stream',
            'X-Accel-Buffering' => 'no',
            'Cache-Control'     => 'no-cache',
        ]);
    }
}

4-3-2 リソースクラスを組み合わせた REST API アプリケーション

HATEOASとは

Hypermedia as the Engine of Application State の略。

APIのレスポンスにも別リソースのリンクを埋め込み、リンクを辿るだけで別アプリケーションの呼び出しを可能にすること。らしい。

ブログで考えると、記事の出力をするAPIにユーザープロフィールや個別のコメントへ投稿者のプロフィールの情報へのURLリンクを埋め込む事で、通常、URLのpathやページの仕様や変化した場合、完全分離されているクライアント(フロント側)ではそれを知る術はないが、リンクが埋め込まれている事で、これらの問題を解決できる。という仕組みと思想的なもののようです。

この考えに対応しているJSONフォーマットとして、[JSON API], [HAL4], [JSON-LD] 等がある

Web APIにはJSONベースのフォーマットを使おう - Qiita

以下は HAL を採用した例となる

引用元
The Hypertext Application Language

{
    "_links": {
        "self": { "href": "/orders" },
        "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }],
        "next": { "href": "/orders?page=2" },
        "ea:find": {
            "href": "/orders{?id}",
            "templated": true
        },
        "ea:admin": [{
            "href": "/admins/2",
            "title": "Fred"
        }, {
            "href": "/admins/5",
            "title": "Kate"
        }]
    },
    "currentlyProcessing": 14,
    "shippedToday": 20,
    "_embedded": {
        "ea:order": [{
            "_links": {
                "self": { "href": "/orders/123" },
                "ea:basket": { "href": "/baskets/98712" },
                "ea:customer": { "href": "/customers/7809" }
            },
            "total": 30.00,
            "currency": "USD",
            "status": "shipped"
        }, {
            "_links": {
                "self": { "href": "/orders/124" },
                "ea:basket": { "href": "/baskets/97213" },
                "ea:customer": { "href": "/customers/12369" }
            },
            "total": 20.00,
            "currency": "USD",
            "status": "processing"
        }]
    }
}

どのようなJSONフォーマットを採用するかは決まっておらす、仕様は様々だが、
Laravel ではこれらのサポート機能として API Resource 機能が提供されている

REST & Hypermedia APIs - Alpha Hydrae

hal+json.png

リソースクラスの基本

resource ってのを make できるらしい
以下のコマンドでapp/Http/Resource ディレクトリ配下にファイルが作成される

php arrisan make:resource UserResource
php arrisan make:resource CommentResource
php arrisan make:resource CommentResourceCollection
php arrisan make:resource ArticleResource

ここではeroquent を使っているが、そうじゃなくてもOKだそうです。
作成した4つのファイルを使って、上の例にある、HATEOASのJSON を出力する例を以下に示します。

chapter04-2/ArticlePayloadAction.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/ArticleResource.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/UserResource.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/CommentResource.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/CommentResourceCollection.php at master · laravel-socym/chapter04-2 · GitHub

ルーター登録

<?php
Route::get('/payload', 'ArticlePayLoadAction');

4-4 ミドルウェア

Laravelにおける、ミドルウェアはコントローラークラスの前後に位置し主にHTTPリクエストのフィルタリングやHTTPレスポンスの変更を行う

4-4-1 ミドルウェアの基本

Laravelのミドルウェアは以下の3種類がある

この流れはフィルタリングなどで使用

HTTPリクエスト --->  ミドルウェア ---> コントローラー

この流れはレスポンス内容の変更や新たなレスポンスの生成が可能

HTTPリクエスト <---  ミドルウェア <--- コントローラー

4-4-2 デフォルトで用意されているミドルウェア

app/Http/kernel.php の App\Http\Kernel クラスで定義されている

グローバルミドルウェア

ルーターに登録されたコントローラークラスが捜査する前に実行される

ルートミドルウェア

デフォルトでwebミドルウェアグループに記述されている

名前付きミドルウェア

ルーターへの登録またはコントローラークラスのコンストラクタなどで任意の名前を指定して利用する

Laravel 標準では多数のミドルウェアが登録されてすでに動作しているが、不要なミドルウェアを削除するとパフォーマンス向上が見込めるケースがあるので必要に応じて使うとよいらしい。

4-4-3 独自ミドルウェアの実装

アプリ固有のミドルウェアを利用するには専用ミドルウェアを実装する必要がある。

ここではリクエストヘッダとレスポンスヘッダにログを書き出すミドルウェア実装を例に解説する。

ミドルウェアクラスの生成

実装例として HeaderDumperクラスを作成する

ミドルウェア作成コマンド

php aritsan male:middleware HeaderDumper

すると app/Http/Middelware ディレクトリ配下に HeaderDumper.php が作られる

リクエストヘッダのログ出力

このクラスでリクエストヘッダをログに書き出す処理の実装例が以下
引用元 : chapter04-2/HeaderDumper.php at master · laravel-socym/chapter04-2 · GitHub

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;

class HeaderDumper
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function handle($request, Closure $next)
    {
        // リクエストheader を log 出力する
        if ($request instanceof Request) {
            $this->logger->info('request', [
                'header' => strval($request->headers)
            ]);
        }
        // 上記同様の結果となるヘルパ関数を使った例
        // info('request', ['header' => strval($request->headers)]);

        /** @var Response $response */
        $response = $next($request);

        // レスポンスheaderをログ出力する
        if ($response instanceof Response) {
            $this->logger->info('response', [
                'header' => strval($response->headers)
            ]);
        }
        return $response;
    }
}

ヘルパ関数 info

レスポンスヘッダのログ出力

上のコードにある後半部分がレスポンスヘッダをログ出力する部分、以下にも記述 レスポンスヘッダを取得するには、ミドルウェアクラス内の $next($request) の戻り値を取得する。

<?php
        /** @var Response $response */
        $response = $next($request);

        // レスポンスheaderをログ出力する
        if ($response instanceof Response) {
            $this->logger->info('response', [
                'header' => strval($response->headers)
            ]);
        }
        return $response;

ミドルウェアの登録

ミドルウェアを作ったら App\Http\Kernel クラスに登録が必要

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\UrlPath::class,
        \App\Http\Middleware\CssFileDate::class,
        // ここに登録しないと動きません seeder の run() とかと同じイメージですね
        \App\Http\Middleware\HeaderDumper::class, // laravel本 4-4 サンプル
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            // 省略
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    protected $routeMiddleware = [
        // routes/web routes/api で使えるアレはココで定義している
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        // 省略
    ];

}

routeMiddeware は routes/web.php や routes/api.php のルーティングで使えるアレを登録できる

<?php
Route::get('admin/profile', function () {
    // $routeMiddleware の auth は下のこれっすね
})->middleware('auth');

おまけ オレオレミドルウェア

以下を参考に作ったものです

キャッシュを有効にしつつ、cssやjsファイルの変更を確実に反映させる – doop

css ファイルって更新してもブラウザ側でキャッシュが読み込まれてしまうケースがあって、開発中に更新してもデザイン変わらず「うぎゃー」ってなることがある。しかし css 読み込みの際にクエリパラメーターを付けて、毎回値を変えれば再読み込みされます。

<link href="http://app_whois.test/css/livelynk.css?q=ここにランダムな値" rel="stylesheet">

しかし、適当なランダム値にすると毎回css呼ばれるのも本番では遅くなって嫌なので、cssファイルの更新日時をクエリのパラメーターにして読み込んでやれば、cssを更新した時だけ、再読み込みされる!というちょっとうれしい奴です。

<link href="http://app_whois.test/css/livelynk.css?q=20190628113106" rel="stylesheet">
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\View;

class CssFileDate
{
    public function handle($request, Closure $next)
    {
        // ファイルpathから更新日を出力、pathにパラメータを入れcssや画像のキャッシュクリアに利用
        $path = public_path();
        $filename = $path . '/css/livelynk.css';
        if (file_exists($filename)) {
            $file_date = date('YmdHis', filemtime($filename));
        } else {
            $file_date =  str_random(6);
        }
        // file_date という変数が view のどこでも使える様になる 
        View::share('file_date', $file_date);
        return $next($request);
    }
}

bladeテンプレート側ではこんな風に使えます

<link href="{{ asset('css/livelynk.css') }}?q={{$file_date}}" rel="stylesheet">

Laravelの単機能を作るまでの大まかな流れ

設計やら命名規則やら一緒くたになってごちゃごちゃとしてます。
また、俺ルールと一般的なルールと、厳密なルールがごっちゃにかいてあります。
が、自分なりに他の人と共通作業を行う際のコンセンサスを取るメモみたいなものをアップしておきます。何かの参考になれば幸い。

(アプリケーションはAPIとし、ごく基本的な設定関連はほぼ終わっている状態と想定)

ルーティング定義をする

routes/api.php

ルートプレフィックスで上層のpathをまとめる
基本アルファベット順で並べる
特別な処理は上か下の行にまとめる

同じControllerの処理はまずCRUDの順で並べ、次にその他の処理を並べる。

基本はControllerで書いたメソッドの順と揃っているのが望ましい

しかし、これだと1行で行けるらしい、他のメソッド入れた際はどうするんだろう?

Route::resource('users', 'UserController');

Controller 作成

コマンド例 : php artisan make:controller HogeFugaController --resource

( --resources オプション CRUDメソッドを自動生成してくれる)

ディレクトリ/命名例 : app/Http/Controllers/HogeFugaController.php

命名規則:

  • 単数形,table名か機能名の UperCamelCase
  • 最後に Controller を書く

アノテーションAPI仕様を書く
requestを受けてserviceに送り返り値をもらう処理を書く
retrun [ ] で想定するAPIの返り値を仮作成
フロント側に要件での最低限の出力を可能とする

unit testファイルを作る

ひとまず pathを指定して
->assertStatus(200) のテストを書く

テストを実施する(時間があればこの辺詳細書きたい)

migration ファイル作成

コマンド例 :
php artisan make:migration create_users_statuses_table

ディレクトリ例 :
app/Http/Controllers/HogeFugaController.php

命名規則 :

  • スネークケース
  • table(複数形)名に続けて create|edit|delete のいずれかを書く
  • 最後に tableを書く

1テーブルの作成・編集・削除につき1ファイルを生成する
migration実施とphpMyAdminでの仕様との差を確認

  • comment->('仮カラム、仮型') 等と通常のコメントで仮である事を明文化しておく
  • コメントアウトをして作らないでおく

factory 作成

migrationファイルと合わせて factoryを作る。

コマンド例 :
php artisan make:factory HogeFugaFactory
ディレクトリ/命名例 :
database/factories/HogeFugaFactory.php

faker を使ったり、Corbon や決め打ちの値で1レコード文の標準的なダミーレコード作成を定義する

seeder ファイル作成

コマンド例 : php artisan make:seeder UserStatusTableSeeder
ディレクトリ/命名例 : database/seeds/UserStatusTableSeeder.php

ダミーレコードを作る為の定義をするファイル
up メソッド内で未定義のカラムがある場合は faker で定義された値でレコードが生成される

upメソッド内の以下の違いに注意、作成時と編集時はメソッド名が異なる
Schema::create( ... )
Schema::table( ... )

忘れがち!
database/seeds/DatabaseSeeder.php

$this->call(UserStatusTableSeeder::class);

を書かないとseedingは実行されない

また、Seeder書いたら以下のコマンドもやっておく  

composer dump-autoload

少なくともレコード1件目はid系カラムのリレーションが成立する現実的値で具体的なデータを入れておく
2件目以降必要な現実的なデータがあれば、個別に生成できるようにしておく

以降のデータは他のtableの連携等含め、リレーションされるidは固定値、その他の値はある程度ランダムで生成されるレコード数を現実的な運用時のページング等の移動が行える程度には入れておく

seeder実施、各カラムに正しくfakeの値が入っているか確認

モデル作成と設定

コマンド例 :
php artisan make:model Models/HogeFuga

ディレクトリ/命名例 :
app/Models/HogeFuga.php

※コマンド時はフォルダpathの指定を間違えないこと

命名規則:

  • 単数形,table名か機能名の UperCamelCase
  • tableの場合は対となるControllerを持つ
  • 最後に Model は書かない

hidden 設定
filiter 設定
table 名指定
リレーション設定

ここにクエリビルダはEloquentの処理は書かない

あとでやる 例外として scpope 等の共通のSQLフィルタ処理のメソッド等を書く

Service 層の作成

app/Http/Services/HogeFugaService.php
※ artisanコマンドでは作れない、コピペで作る

命名規則:

  • Controllerと対にする
  • 単数形,table名か機能名の UperCamelCase
  • 最後に Service を書く
    • Controller が HogeFugaController なら
    • Service は HogeFUgaService となる

必要なら Controllerから受けたrequestを受ける
1行で済むクエリビルダやEloquentならここに書く

必要なビジネスロジック処理を書く
複数行に渡るDB処理は、 Repositoryに渡して返り値をもらう処理を書く 引数を渡してRepository処理に反映させても良い

必要であれば他のtable層のRepository層を呼び出しても良いがなるべくしない

Repository 層の作成

app/Http/Repositorys/HogeFugaRepository.php
※ artisanコマンドでは作れない、コピペで作る

命名規則:

  • 単数形,table名か機能名の UperCamelCase
  • 最後に Repository を書く
  • Serviceと対にする
    • Controller が HogeFugaController
    • Service が HogeFUgaService なら
    • Repository は HogeFugaRepository となる

use App/Models/Hoge
use DB
を書く

DBから値を拾う処理のみを書く
簡単な if分岐やswitch 等はOK
メソッドにどのServiceのメソッドからの呼び出しかコメントを書く

引数を渡して処理を変える等しても良いが、詳細やルールをコメントする

Requests (フォームリクエスト)によるバリデートの作成

コマンド例 : php artisan make:request StoreBlogPost

ディレクトリ/命名例 : app/Htp/Requests/HogeFugaCreareUpdate.php

命名規則:

  • 単数形,table名か機能名の UperCamelCase Controller と同じ
  • 続けて対となるControllerのメソッド名
  • create, update 等、複数のメソッドで共通化できるものは両方の名前を続けて書く

authorrize() は、ほぼ true となる

rules() にバリデート条件を書く、仕様書に合わせて書く
特殊なバリデートが必要な箇所はオリジナルバリデートを作成する

本来はこのへんでしっかり test を書いて検証をしたい所だが、ひとまず、値を入れてみて手動のpost,get,put,delete,のtestをする


ひとまずこんな感じ。

vagrant Homestead でホストOSの共有フォルダが見れなくなった際の対処

状況

ある日Homesteadの vagrant up 時に以下の様なエラーが出てゲストOSとホストOSフォルダ共有が出来なくなってしまった。 (windows10 64bit環境)

Going on, assuming VBoxService is correct...
bash: line 5: setup: command not found
==> homestead-7: Checking for guest additions in VM...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

 setup

Stdout from the command:



Stderr from the command:

bash: line 5: setup: command not found

エラーでググると以下のページに行き当たり同じ対処をしたが、 GuestAdditions の インストールをHomestead側で行ってもmountがされない様だった。

対応したが私の環境では解決しなかった方法

qiita.com

qiita.com

上記のページに習いmountしてみたが上手くいかなかった。

vagrant@homestead:/mnt$ sudo ./VBoxLinuxAdditions.run
Verifying archive integrity... All good.
# ... インストール中のメッセージ...
VirtualBox Guest Additions: Running kernel modules will not be replaced until
the system is restarted
vagrant@homestead:/mnt$ cd
vagrant@homestead:~$ ls /etc/init.d/ | grep vboxadd
# 何も出ない! 入ってない!
vagrant@homestead:~$ ls /etc/init.d/
acpid     avahi-daemon      cron              ebtables     iscsid             lvm2-lvmetad   mdadm           nfs-common  open-vm-tools  php7.2-fpm    postgresql    rsync           supervisor  unattended-upgrades
apparmor  beanstalkd        cryptdisks        grub-common  keyboard-setup.sh  lvm2-lvmpolld  mdadm-waitidle  nginx       php5.6-fpm     plymouth      procps        rsyslog         sysstat     uuidd
apport    blackfire-agent   cryptdisks-early  hwclock.sh   kmod               lxcfs          memcached       ntp         php7.0-fpm     plymouth-log  redis-server  screen-cleanup  udev        x11-common
atd       console-setup.sh  dbus              irqbalance   lvm2               lxd            mysql           open-iscsi  php7.1-fpm     postfix       rpcbind       ssh             ufw


# やはり vboxadd がいない!

ちなみにこの記事執筆時のバージョンは以下 6.0.8 でした。 Index of /virtualbox/6.0.8

wget http://download.virtualbox.org/virtualbox/6.0.8/VBoxGuestAdditions_6.0.8.iso


そこでHomesteadのBOXファイル自体が古く対応できていないのでは?と思い至りupdateすることにした

hnakamur.github.io

windows側 の Gitbash で以下のコマンドを実行

$ vagrant box list

# その他のBOXファイルがここに表示されている
# updateを重ね現在は 6.1.0 
laravel/homestead                  (virtualbox, 5.0.1)
laravel/homestead                  (virtualbox, 5.2.0)
laravel/homestead                  (virtualbox, 6.1.0)

$ vagrant box update --box laravel/homestead

# ここでダウンロードとupdateが行われるがしばらく時間がかかる


$ vagrant box list
# その他のBOXファイルがここに表示されている
laravel/homestead                  (virtualbox, 5.0.1)
laravel/homestead                  (virtualbox, 5.2.0)
laravel/homestead                  (virtualbox, 6.1.0)
laravel/homestead                  (virtualbox, 8.0.0-beta)

# インストール後再度確認、上記の様に8.0.0-betaが入った

しかし、無事updateされて晴れてvagrant upしたが駄目。再度 GuestAdditions をmountしても同じ結果となる。乗らない…。

問題点が判明

windows側のGit bash で状況確認中、以下のerrorが出た事に注目

$ vagrant vbguest
vagrant vbguest [homestead-7] GuestAdditions seems to be installed (6.0.8) correctly, but not running. bash: line 5: setup: command not found The following SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed!

もう直接的に GuestAdditions 6.0.8が駄目っす!って言っている。犯人はお前か。

このerror文章で検索をかけてみた所、以下のページが見つかる。

github.com

リンク先引用

[devapp] GuestAdditions seems to be installed (6.0.6) correctly, but not running. Redirecting to /bin/systemctl start vboxadd.service Redirecting to /bin/systemctl start vboxadd-service.service bash: line 4: setup: command not found ==> devapp: Checking for guest additions in VM... The following SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed!

setup

Stdout from the command:

Stderr from the command:

bash: line 4: setup: command not found ```

6.0.6でも同じ問題があったようだ

で、以下のコメントに注目

alvaro-canepa commented on 20 Apr

I have the same issue with Homestead and Virtualbox 6.0.6.

Adding this to Vagrantfile solve the problem:

if Vagrant.has_plugin?("vagrant-vbguest")
    config.vbguest.auto_update = false  
end

alvaro-canepa さんが、Vagrantfileにこの設定を追加しろって言ってる。 このコードはつまり vagrant-vbguestはアップデートさせず使えって設定を書けってことらしい。

結論

という事で Homestead内の Vagrantfile を開き if文の中に設定を追記

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

    # ここには他の設定諸々が書かれている

    # 2019/06/24 add https://github.com/dotless-de/vagrant-vbguest/issues/333
    if Vagrant.has_plugin?("vagrant-vbguest")
        config.vbguest.auto_update = false  
    end
end

これでvagrant reload --provisionした所、諸々の再設定がされて無事共有フォルダが反映されるようになりました。 でもいつかupodateが反映され、 その際には vagrant-vbguest も正しく修正された暁には、上の設定は消して新たな設定を反映されるようにするのが望ましいでしょう。

Apache2.4で複数ドメインを1つのサーバーで動かして Let's Encrypt でssl対応する方法のメモ

Apache2.4で複数のドメインを当てて、Let's Encryptで二つのサイトでssl対応をした際のメモを記しておきます。

  • 要約
    • Let's Encryptで複数のドメインで証明書を取るにはコマンドがあるが、既に一つのSSL認証を取得している場合は一度失効と削除をする必要があるようです。
    • /etc/http/conf/httpd.conf の記述を /etc/httpd/conf.d/ssl.conf に転載するのが基本
    • Let's Encryptで取得したキーのありかを conf ファイルで指定してやらないと駄目
    • 何度も設定間違って再取得していると、1時間、または1週間のデッドロックを食らう
    • ドメインのアクセスには http:// https:// www. 有り www.無しの合計4パターンがあるが、今回はhttps:// のwww無しで統一リダイレクトをさせる設定にした


Let's Encryptで複数のドメインで証明書を取るコマンド

既に一つのSSL認証を取得している場合は一度失効と削除をする必要があるようです。 削除の方については以下のサイト等を参考にさせていただきました。

ajicolor.hatenablog.jp

yoshinorin.net

curecode.jp



2つのサイトでのSSL認証の取得コマンド rootユーザーになって以下のコマンドを打ちます。

例はドメイン aaa.site, bbb.siteの二つとしています。 サイトの公開フォルダはサーバー内のそれぞれ /var/www/aaa-site, /var/www/bbb-siteにあるものとします。

# certbot certonly --webroot -w /var/www/aaa-site -d aaa.site  -w /var/www/bbb-site -d bbb.site

なお、インストールの経緯でコマンドの最初は certbot-auto 等にもなるようです。

次にApacheの設定ファイルです。
/etc/httpd/conf/httpd.conf

NameVirtualHost *:80

# 一つ目のサイトの設定
<VirtualHost *:80>
    DocumentRoot /var/www/aaa-web-site
    ServerName aaa.site
    # エイリアスの指定で www 付きでのアクセスも受け入れる事が出来るようです。
    ServerAlias www.aaa.site
    AddDefaultCharset UTF-8
    <Directory "/var/www/aaa-web-site/">
        AllowOverride All

        # リダイレクト処理を行う設定
        RewriteEngine On

        # 以下の二つのドメインでアクセスがあった場合書き換え処理を行う
        # http://aaa.site と http://www.aaa.site でアクセスがあった場合、リダイレクトをする
        RewriteCond %{SERVER_NAME} =aaa.site [OR]
        RewriteCond %{SERVER_NAME} =www.aaa.site

        # リダイレクト先は https://aaa.site となる、URIのパラメーターがある場合はそのままで飛ばす
        # 301リダイレクト LはマッチしたらRewriteを止め以降のルールは無視するそうです
        RewriteRule ^ https://aaa.site%{REQUEST_URI} [R=301,L]

    </Directory>
</VirtualHost>

# ふたつめのサイトの設定 やっている事は同じです
<VirtualHost *:80>
    DocumentRoot /var/www/bbb-web-site
    ServerName bbb.site
    ServerAlias www.bbb.site
    AddDefaultCharset UTF-8
    <Directory "/var/www/bbb-web-site/">
        AllowOverride All
        RewriteEngine On
        RewriteCond %{SERVER_NAME} =bbb.site [OR]
        RewriteCond %{SERVER_NAME} =www.bbb.site
        RewriteRule ^ https://bbb.site%{REQUEST_URI} [R=301,L]
    </Directory>
</VirtualHost>



/etc/httpd/conf.d/ssl.conf
かなり前に行ったLet's Encryptが自動生成した記述があるかは不明。長い記述の一部分のみを記載

NameVirtualHost *:443
# 一つ目のサイトの設定
<VirtualHost _default_:443>
    SSLEngine on
    DocumentRoot /var/www/aaa-web-site
    ServerName aaa.site
    ServerAlias www.aaa.site

    # SSL認証キーのありかを指定する Let's Encrypt で必要な設定
    # ファイルの場所はCentOS7の私の環境の場合です。環境によって異なる可能性があります。
    SSLCertificateFile /etc/letsencrypt/live/aaa.site/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/aaa.site/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/aaa.site/chain.pem

    <Directory "/var/www/aaa-web-site/">
        AllowOverride All
        AddDefaultCharset UTF-8
        RewriteEngine On

        # https://www.aaa.site でアクセスがあった場合のリダイレクト設定になる
        RewriteCond %{SERVER_NAME} =www.aaa.site
        # https://aaa.site へリダイレクトする、URIのパラメーターがある場合はそのままで飛ばす
        RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI}  [END,NE,R=permanent]

    </Directory>
</VirtualHost>

# ふたつ目のサイトの設定 やってることは同じですが、
# 証明書キーの設定は1つめのサイトで行っているので必要無いのがミソです。
<VirtualHost _default_:443>
    SSLEngine on
    DocumentRoot /var/www/bbb-web-site
    ServerName bbb.site
    ServerAlias www.bbb.site

    <Directory "/var/www/bbb-web-site/">
        AllowOverride All
        AddDefaultCharset UTF-8
        RewriteEngine On

        RewriteCond %{SERVER_NAME} =www.bbb.site
        RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI}  [END,NE,R=permanent]

    </Directory>
</VirtualHost>

参考にさせていただいたサイト

weblabo.oscasierra.net

oxynotes.com

jitaku-server.com

qiita.com

www.netassist.ne.jp

hi3103.net

www.yoheim.net

近況報告

最近あんまり書いて無いですが、色々始まってます。

最近、講師から職業プログラマージョブチェンジを果たしました

5月の連休明けから、開発(PHP)の委託業務を週3日で始めました。通勤1.5時間がツライですが、お仕事をしつつえらい勉強になってます。

残りの2日は毎年やってる大学の非常勤講師(1年生にコンピュータの基礎を教える)ですが今年は色々あり過ぎて正直しんどい状況…。

現状の目標は8月から週5で通勤が辛くないPHP,Laravelのお仕事を探す事です。去年作ったwi-fiで滞在者がわかるアプリLivelynkを売るのではなく、あれを作った自分を売る、という風に視点を変えたら、40過ぎてからプログラミングを始めた人でも、けっこう高い価値が付いたみたいです。

滞在者確認アプリの現状

そのLivelynkの改修がなかなか進みませんが、ギークオフィス恵比寿ではインフラ的な位置を完全に確立して現在も絶賛稼働中です。 実はドメインの設定でやらかし連休中に数日サービスが繋がらない状態が続いたのですが、その際の不便な事といったら!みたいな感覚になったのが、申し訳なさもありますが、大事なものになっている事を認識出来た良い機会でもありました。

あの記事を書いた後から付いた機能があります。

一つはGoogleHomeとの連携、現在は人が来訪すると挨拶をさせている程度ですが、今後、価値ある情報を提供させることをするツモリです。

Livelynkサーバーからラスパイを介して、GoogleHomeにしゃべらせるというハック的な実装の詳細はともかくですが、静かなオフィスで唐突にGoogleHomeが余計な挨拶や決まりきった挨拶をする際の気まずさといったら…。新たなソリューションでこれをやってはいかん!というのがわかっただけでも大きな収穫でした。

そしてもう一つはこの場所に『行く予定』を宣言できる機能です。 元々同じ仲間の兄弟アプリ『ツモリンク』

www.tumolink.com

というのがありまして、任意の場所に今から行こうかな?というあいまいなニュアンスを宣言するアプリなのですが、この宣言ができる機能のみLivelynkに内包して実装しました。

f:id:sakamata:20190529114722p:plain

実装の際は新機能の部分のみですが、習ったばかりのテスト駆動開発を意識して、テストを先に書いて実装をして、テストが通れば実装もOK、というフローで作りました。慣れずに時間はかかりましたが、今の所大きなバグは無い筈です。(つい昨日細かな不具合報告があったけど…)

あと、簡単な実装ですが、Googleカレンダーをメインの画面に表示させる様にして、近日中のギークオフィス恵比寿のミーティングやイベントが、一目でわかる様にしました。

f:id:sakamata:20190529114825p:plain

また、Livelynkは企画や機能として何か重要なパーツが欠けている気がしているので、細かな機能追加をして使いやすさを追求しつつ、新しい価値を提供できる様なものにして行くツモリです。

その他お仕事や今後の事

また、小規模ですがプログラミングやウェブサイト制作のお仕事もいただいています。あと、現在週一で1時間だけ、プログラミング初心者の人に基礎を教えるというビデオチャットのアルバイトもしてます。という事でWord,やExcelだけでなくプログラムを人に教えられる所まで、ちょっとだけ来たみたいです。

でも、人に教える仕事ってものすごいやりがあって、自分も勉強になるのですが、時間ばかりかかってなかなか実入りが少ないのが悩み所ですね。

という事で、遠い目標として通貨や評価の新しい価値体系を作る為に、現在からの自分の価値を最大限発揮できるに様にするにはどうしたらいいのか?みたいな事を考え始めてます。

ゆるふわLaravel勉強会 (認証/JWT) 認証に関する資料

Laravel 認証についての色々まとめ

以下の記事は 2019/4/1 コワーキングスペース秋葉原Weeybleで行われる輪読会 [秋葉原] ゆるふわLaravel勉強会 (認証/JWT)のための認証に関する資料となります。

内容は以下の有志によるリファレンスサイトの記事の要約となります。 Laravel 5.8 認証

また、バージョンはLaravel 5.8.8 を前提にしています。

認証クイックスタート

Laravelインストール直後は認証系がフロント側で動く状態にはなっていないが、Controller等は既に準備されている
Controllers/Auth配下

コントローラー 用途
RegisterController 新ユーザーの登録
LoginController 認証処理
ForgotPasswordController パスワードリセットのためのメールリンク処理
ResetPasswordController パスワードリセット処理

ひとまず認証付きのアプリを作るには、まずは以下のコマンドを打って、フロント側やルーティングに認証系の処理を自動生成させる

php artisan make:auth

コマンドを叩くとファイルに記述が追加されたり、新規ファイルが作られたりする
もし認証付きのアプリケーションを作るのであればfirst commit直後位に実施してしまうのが良い

変化のあるファイルの紹介

ルーティング
routes/web.php

// 以下2行が追加される
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');

Auth::routes();で登録画面、ログイン画面、パスワードリセットのすべてのルーティングを設定してくれている、個別に編集が必要な場合は、この行を廃止して画面毎にルーティングを定義する。


HomeControllerの追加
app\Http\Controllers\HomeController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

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

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('home');
    }
}

ルーティングの2行目に追加された処理を行うControllerだが、ログイン直後の画面のサンプル例となる。 ログインすると/homeに移動するので、アプリケーションの仕様に従い、表示を作り込めば良いし、Homeという名前が気に食わないなら随意に変更する


その他以下の各viewファイルが自動生成されます。

resources\views\home.blade.php
resources\views\auth\login.blade.php
resources\views\auth\register.blade.php
resources\views\auth\verify.blade.php
resources\views\auth\passwords\email.blade.php
resources\views\auth\passwords\reset.blade.php
resources\views\layouts\app.blade.php

ログイン・登録・パスワードリマインダ等のページと機能も自動で生成してほぼ機能するようになります。

ブラウザでルートのpathにアクセスすると、画面左上に[LOGIN]と[REGISTER]のリンクが表示されるようになります。


認証の動作確認

まずはDBが無いので作ります。(vagrant環境でMySQLがある前提)

$ mysql -u root -p secret
mysql> create database your_database_name;
mysql>exit

migrateしてDBにtableを作ります。

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

user table以外にpassword resetのtableもcreateされました。
これでREGISTER出来る様になりました。
登録画面に移動して、登録すると、DBにuserが生成され、ログインして/homeにリダイレクトされます。
当然一度ログアウトしてログインができることも確認できます。
また、ログイン画面にパスワードを忘れた際のMailリマインダ―がありますが、mailの設定をしないと飛びませんので、今回は割愛

以上が、クイックスタートで作られた認証の初期概要です。


認証のカスタマイズ (初級編)

初期状態は以下の様な認証の仕様となっていますが、これは簡単に変更が可能です。
以下はリファレンスサイトの内容をほぼ転載しています。

ログイン後のリダイレクト先 /homeの変更

LoginControllerRegisterControllerResetPasswordControllerVerificationControllerredirectToプロパティで、認証後のリダイレクト先の場所を定義してください。

protected $redirectTo = '/';


ログイン時のEmailをユニークな username, user_id,等に変更する

ログイン時はemailとpasswordの組み合わせが認証のデフォルトだが、emailをuser_id等に変更したい場合。

これをカスタマイズしたい場合は、LoginControllerusernameメソッドを定義してください。

public function username()
{
    return 'user_id';
}

当然user Tableをmigrateして変更したいユニークとなるカラムを追加してください。


登録済みユーザーのロール別制限 guard メソッド

webアプリを普通に作ってる場合は個人的にあまり使いませんが、APIやSPAの際にはよく使う事になるそうです。

LoginControllerRegisterControllerResetPasswordControllerguardメソッドを定義してください。メソッドからガードインスタンスを返してください。

use Illuminate\Support\Facades\Auth;

protected function guard()
{
    return Auth::guard('guard-name');
}


認証済みユーザーの取得 -Authファサード超便利-

Auth::user()->email とかでuser関連のデータをControllerやviewですぐ取得できる。
基本は認証時に行った user Tableのカラムのデータが取得できるので、自分のデータを取得したい際にとても便利に使えます。

use Illuminate\Support\Facades\Auth;

Auth::user()->email; // taro@gmail.com


認証中のユーザーか調べる -必須並みの便利機能-

Auth::check() これも 認証してる/してない を簡単に切り替え判断できる。controllerでもviewでも使える。認証の有無で処理や表示を変える際に便利!よく使う。

use Illuminate\Support\Facades\Auth;

if (Auth::check()) {
    // ログイン中の場合の処理
} else {
    // 非ログイン中の処理
}


認証済みのみ通すページをルーティングで指定

この辺はルーティングの説明時にも紹介した内容で、ルーティングrouter/でチェーンメソッド->middleware('auth')と書くと、認証時のみ有効となるルーティングとして定義できます。

Route::get('profile', function() {
    // 認証済みのユーザーのみが入れる
})->middleware('auth');

それ以外でも例えばコントローラのコンストラクタでもmiddlewareメソッドを呼べる

public function __construct()
{
    $this->middleware('auth');
}


認証回数制限

brute-force対策が最初から出来ている感じ?

Laravelの組み込みLoginControllerクラスを使用している場合、Illuminate\Foundation\Auth\ThrottlesLoginsトレイトが最初からコントローラで取り込まれています。デフォルトでは何度も正しくログインできなかった後、一分間ログインできなくなります。制限はユーザーの名前/メールアドレスとIPアドレスで限定されます。

自前のユーザー認証 (中級編)

リファレンスにある内容を紹介する
app\Http\Controllers\Auth\LoginController.php
authenticateメソッドを新たに定義する。

認証系のカスタマイズはvendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.phpにあるメソッドをオーバーライドするのが応用編の入り口の様です。

ちなみに下記のIlluminate\Foundation\Auth\AuthenticatesUsers.php内のauthenticatedメソッドの実体は空メソッドで、カスタマイズ専用のメソッドである事がわかります。

    /**
     * The user has been authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  mixed  $user
     * @return mixed
     */
    protected function authenticated(Request $request, $user)
    {
        //
    }

これを以下の様にLoginController.phpに追加で記述をします。
以下、リファレンスサイトの例を転載します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    /**
     * 認証を処理する
     *
     * @param  \Illuminate\Http\Request $request
     *
     * @return Response
     */
    public function authenticate(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            // 認証に成功した
            return redirect()->intended('dashboard');
        }
    }
}

ここで重要なのはAuth::attempt($credentials)です。
これが認証するか否かを判定できる仕組みで、引数に渡すのはモデルのカラム名となります。

オレオレの実装例

    public function authenticate(Request $request)
    {
        // login_id カラムは email + '@' + community_id(int) で構成されたユニークの文字列として登録時に保存された値、これでログイン認証を行う
        $login_id = $request->email . '@' . $request->community_id;
        $credentials  = array(
            'login_id' => $login_id,
            'password' => $request->password,
        );
        $request->validate([
            'email' => 'required|string|email|max:170',
            'password' => 'required|string|min:6',
        ]);
        if (Auth::attempt($credentials)) {
            return redirect('/')->with('message', 'ログインしました');
        } else {
            return redirect()->back()->withErrors(array('email' => 'E-mailかPasswordが正しくありません'))->withInput();
        }
    }

Auth::attempt($credentials)に渡す認証の値は追加が可能です。以下の様に認証時の条件を3つ以上に設定することができます。

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // ユーザーは存在しており、かつアクティブで、資格停止されていない
}

以降の認証に関する記述はリファレンスサイトを参考にしてください。
これ以降はケースバイケースで使用するかも。といったものが多い印象です。


よくありそうなカスタマイズについての実例など

RegisterController のカスタマイズ

ユーザー登録を行う際のvalidatorcreateメソッドの変更が必要であれば変える。
この辺は大変解りやすいコードだし、見たまんまで弄ってしまって基本OKです。
バリデートの変更や、ユーザー登録時に発行すべきカラムのデータ等を生成します。ありがちなのはユーザーの権限を初期状態で追加する。等の処理を行う事になるかと思います。

app\Http\Controllers\Auth\RegisterController.php抜粋

    /**
     * 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']),
        ]);
    }


認証カスタマイズ(オレオレ編)

実はAuth::user()で呼べるのは通常,user Tableのカラムだけとなります。ところがアプリの仕様上 user Tableがユニークにならない様な場合は、Auth::user() で欲しい一意のユーザーのデータが取得できず、偉い苦労しました。

どんなことをやったかというと、この記事にあるような事をしました。

Laravel 認証カスタマイズ 複数tableを結合しての認証で Auth::user() に必要な値を入れる方法

Laravelの認証機能をカスタマイズして、認証時に3つのカラム条件で認証をし、さらに認証後にAuth::user() ファサードに複数tableからの値を取得できるようにしました。

という訳で後半はこれについて説明します。

Laravel 認証カスタマイズ 複数tableを結合しての認証で Auth::user() に必要な値を入れる方法

Laravelの認証機能をカスタマイズして、認証時に3つのカラム条件で認証をし、さらに認証後にAuth::user() ファサードに複数tableからの値を取得することをしました。

環境Laravel5.6
windows10Pro 64bit
vagrant環境にて実施

そもそもの経緯として、DB構造が特殊になり、 通常の users tableのみでの認証では必要な値を取得できなくなっていました。
全体のDB設計をしてからやれば済む問題だったのですが、単数のtableでの認証を前提として、システムを作り込んでしまい。後からどうしても複数tableからの値を取得する必要に迫られて、Laravelの認証系のドキュメントやサイトをかなり読込むことになりました。

結果を先に書いておくと 複数tableをjoinしたMySQLのviewを migration で書いて、そのviewを認証用table,auth_usersとして登録し、viewから一意の値を導きます。
通常はusers tableから、 email, password と2種類のカラムから一意、かつパスワードの一致で認証をしますが、今回は unique_name, password, community_id の3つのカラム条件から認証条件を導き出しています。

table構造図

パワポで書いたのでちょっと変ですが意図は伝わるかと思います。)
f:id:sakamata:20181116045527p:plain

user は複数のコミュニティに任意に所属・登録できるというシステムで、これを中間tableである community_user でどのユーザーがどのコミュニティに所属しているかを管理しています。

さらに community_user tableにはと1対1(hasOne)の関係で追加table communities_users_statuses があり、特定のユーザーの特定のコミュニティ内での情報(権限レベルのidや表示設定、日時等)が保存されています。さらにその先には roles(権限)tableがあり、idと権限名が記載されているという、かなりがっつりなtable設計をした構造になってます。

これら

users
community_user
communities_users_statuses
roles

上記4つのtabelのからのカラムデータを、Laravelのファサード Auth::user()->name といった具合に、 Auth::user()->role も Auth::user()->user_idAuth::user()->community_id も "->"を一つ書くだけで一気に取得できる様にして、その値をアプリケーションの様々な処理のトリガーとして利用したいのです。(というかそういう風に作ってしまって後から変えるの凄いしんどい。)

まず、通常のLaravelの標準の認証系の追加をさくっとします。この辺は他のサイトにお任せします。

Laravel 5.6 認証 イントロダクション

で、色々ファイルが作られますので、それをどう変えるか、というお話です。

まず、MySQLのviewを auth_users という名前でマイグレーションファイルで作成します。通常のマイグレーションファイルの記述方法とは少し異なります。

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

// 認証時に使用される view  Auth::user() に収納される
// $credentials array ('id', 'unique_name', 'password' )
class CreateViewAuthUsersTable20181114 extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        DB::statement( 'DROP VIEW IF EXISTS auth_users' );
        DB::statement( "
            CREATE VIEW auth_users AS
            SELECT
                community_user.id,
                community_user.user_id,
                community_user.community_id,
                users.name,
                users.unique_name,
                users.email,
                users.facebook_id,
                users.password,
                users.remember_token,
                communities.user_id AS reader_id,
                communities.name AS community_unique_name,
                communities.service_name,
                communities_users_statuses.role_id,
                communities_users_statuses.hide,
                communities_users_statuses.last_access,
                communities_users_statuses.created_at,
                communities_users_statuses.updated_at,
                roles.role
            FROM community_user
            JOIN users ON (community_user.user_id = users.id)
            JOIN communities ON (community_user.community_id = communities.id)
            JOIN communities_users_statuses ON (community_user.id = communities_users_statuses.id)
            JOIN roles ON (roles.id = communities_users_statuses.role_id)
        " );
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        DB::statement( 'DROP VIEW IF EXISTS auth_users' );
    }
}

がっつりとview作成のSQL文を書いています。
DB::statement は通常のSQL文を書くという事ですね。 宣言の部分に use DB; は書かなくても大丈夫でした。書いたら逆にエラーになった。
で、以下のコマンドを実施

php artisan migrate

SQL文を走らせて view,auth_usersを作ります。
この方法はこちらのサイトを参考にさせてもらいました。

LaravelのマイグレーションでView Tableを作成する

これにより先ほど説明した4つのtableが結合されたviewが作成されます。
f:id:sakamata:20181116045531p:plain

viewは一つの大きなtableの様に扱う事ができます。出来上がったviewには所により同じ値が2度以上でますが、 unique_name password community_id の3つの値が全て重複するレコードは存在せず、これにより一意の認証が可能となります。

次にこのviewを認証用のtableとして使うための認証用モデルを作ります。
これは、標準で認証に使われているファイル app\User.php をコピーして書き換えました。

app\AuthUser.php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class AuthUser extends Authenticatable
{
    use Notifiable;

    /**
     * モデルと関連しているテーブル
     *
     * @var string
     */
    protected $table = 'auth_users';

    /**
     * The attributes that should be hidden for arrays.
     * JSON出力の際誤って含めいない属性という事らしい
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    // 日時表記変更の ->format('Y-m-d') を使いたいカラム名を指定する
    protected $dates = [
        'last_access',
        'created_at',
        'updated_at',
    ];
}

これが結構忘れがちですが大事です!

次に上記の app\AuthUser.php を認証時に使うように config ファイルの model を書き換えます。

config/auth.php (抜粋)

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\AuthUser::class,
        ],

また、login画面のviewファイルにはコミュニティ毎にログイン画面のURLが異なる仕様となっています。
ここにformの hidden で表示されているログイン画面の community_id の値をPOSTできる様にしています。また、標準のemailの入力欄であるフォームを ユーザーID(unique_name)の入力欄に変更しています。

resources\views\auth\login.blade.php

省略
<form method="POST" action="{{ route('login') }}" aria-label="{{ __('Login') }}">
@csrf
<input type="hidden" name="community_id" value="{{$community->id}}">
省略
<input id="unique_name" type="text" class="form-control{{ $errors->has('unique_name') ? ' is-invalid' : '' }}" name="unique_name" value="{{ old('unique_name') }}" required autofocus>

最後?に LoginController を書き換えます。

app\Http\Controllers\Auth\LoginController.php (抜粋)

// 前後省略
use DB;
use Illuminate\Support\Facades\Auth;

//Class宣言部分 省略

    // ログイン時に使用するユニークであるカラムを指定
    public function username()
    {
        return 'id';
    }

    public function authenticate(Request $request)
    {
        $request->validate([
            'unique_name' => ['required', 'string', 'min:6', 'max:40',  'regex:/^[a-zA-Z0-9@_\-.]{6,40}$/u'],
            'password' => 'required|string|min:6|max:100',
        ]);

        //  該当の community_user の id を取得
        $community_user_id = DB::table('community_user')
            ->leftJoin('users', 'users.id', '=', 'community_user.user_id')
            ->where([
                ['unique_name', $request->unique_name],
                ['community_id', $request->community_id],
        ])->pluck('community_user.id')->first();

        if (!$community_user_id) {
            // 他のコミュニティで認証が取れるか?
            $result_bool = $this->CheckOtherCommunityExists($request->unique_name, $request->password);
            if ($result_bool) {
                // 他のコミュニティにいる場合中間table等にレコード追加
                $community_user_id = $this->InsertNewStatuses($request->community_id, $request->unique_name);
            } else {
                // 他のコミュニティにいない場合
                return redirect()->back()->withErrors(array('unique_name' => 'ユーザーIDかPasswordが正しくありません'))->withInput();
            }
        }

        // community_user_id を含めた通常の承認フロー
        $credentials  = array(
            'unique_name' => $request->unique_name,
            'password' => $request->password,
            'id' => $community_user_id,
        );
        // 認証許可
        if (Auth::attempt($credentials)) {
            // session にcommunity値保存
            $request->session()->put('community_id', $request->community_id);
            $request->session()->put('community_user_id', $community_user_id);
            return redirect('/')->with('message', 'ログインしました');
        } else {
            return redirect()->back()->withErrors(array('unique_name' => 'ユーザーIDかPasswordが正しくありません'))->withInput();
        }
    }

通常は$credentials の配列に email と password のみを指定して Auth::attempt($credentials) と渡してやれば認証を結果を返してくれますが、auth_usersから一意の値として取得できる条件として id を配列に追加してやります。

        $credentials  = array(
            'unique_name' => $request->unique_name,
            'password' => $request->password,
            'id' => $community_user_id,
        );

こうすることにより、 MySQLのviewで作られた auth_users table を探しに行き、一意の値を取得して認証が行われるという訳です。
ちなみにコードの中ほどの独自メソッド群は、$community_user_id を特定する処理として、認証前に値を探しに行ったり、ユーザーがまだログインしたことのないコミュニティに初ログインした際に既存ユーザーであるか?の確認をしたり、community_user にtableに値を追加したり、と、このWebアプリケーション独自の処理を色々してます。

こっからは蛇足ですが、その処理の際に、認証を全て自分でカスタマイズできそうなヒントになりそうな処理を書いたので紹介。

    // return bool 他のコミュニティに存在するかを判定する
    public function CheckOtherCommunityExists($unique_name, $password)
    {
        $hash_password = DB::table('users')->where([
            ['unique_name', $unique_name],
        ])->pluck('password')->first();

        if ($hash_password) {
            if (Hash::check($password, $hash_password)) {
                return true;
            }
        }
        return false;
    }

上記の自作メソッドは 認証時の id と password を使って、クイックスタートで作られる認証系の処理を通さないで ユーザー認証の判定ができています。(unique_name とpassword)この処理を書いていたら図らずも結構普通な感じの認証処理になってました。 なので、単純に自分で認証処理を作りたい際は上記を参考に返り値となるboolの判定を基に Auth::login($user);

とやってしまえば、ログイン認証っぽい事はできるようです。 $user はAuth::user() に入る一意のユーザーのオブジェクト(DBのrecord)ですね。ただ、tokenに値を入れたり、リダイレクト先を指定したりの処理がどこまで必要になるかは未検証です。

Laravel 5.6 認証 イントロダクション
https://readouble.com/laravel/5.6/ja/authentication.html